urlkit

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Mar 11, 2026 License: MIT Imports: 12 Imported by: 6

README

go-urlkit

A Go library for URL routing and management with Express.js style route templates. Provides a type-safe way to build URLs with parameters and query strings, organized into logical groups.

Breaking API

go-urlkit now defaults to safe runtime mutation:

  • conflicting route redefinitions fail with typed errors by default
  • mutating APIs return errors instead of silently replacing routes
  • NewRouteManager only creates empty managers
  • NewRouteManagerFromConfig is the only config-loading constructor
  • dotted root group registration is rejected; use nested groups or EnsureGroup

Installation

go get github.com/goliatone/go-urlkit

Features

  • Express.js style route templates with parameter substitution
  • Route organization by groups (frontend, backend, webhooks, etc.)
  • Template-based URL generation with variable inheritance
  • Fluent builder API for URL construction
  • Built in validation for route configuration
  • Type safe parameter and query handling
  • URL encoding and query string management
  • Complete OAuth2 client with state management and encryption

Quick Start

package main

import (
    "fmt"
    "github.com/goliatone/go-urlkit"
)

func main() {
    // Create a route manager
    rm := urlkit.NewRouteManager()

    // Register route groups
    api, _, err := rm.RegisterGroup("api", "https://api.example.com", map[string]string{
        "user":     "/users/:id",
        "profile":  "/users/:id/profile",
    })
    if err != nil {
        panic(err)
    }

    url, _ := api.Render("user", urlkit.Params{"id": "123"})
    fmt.Println(url) // https://api.example.com/users/123

    // Using the builder pattern
    url = api.Builder("profile").
        WithParam("id", "123").
        WithQuery("tab", "settings").
        MustBuild()
    fmt.Println(url) // https://api.example.com/users/123/profile?tab=settings
}

Core Types

RouteManager

Central manager for organizing route groups.

rm := urlkit.NewRouteManager()
group, result, err := rm.RegisterGroup("group-name", "base-url", routes)
_ = group
_ = result
_ = err
Runtime Mutation Safety
rm := urlkit.NewRouteManager()

_, _, err := rm.RegisterGroup("api", "https://api.example.com", map[string]string{
    "status": "/status",
})
if err != nil {
    panic(err)
}

_, result, err := rm.AddRoutes("api", map[string]string{
    "status": "/health",
    "users":  "/users/:id",
})
if err != nil {
    // err is RouteConflictErrors by default
    fmt.Println(err)
}
fmt.Printf("added=%v conflicts=%v\n", result.Added, result.Conflicts)

Use explicit policies when you want different behavior:

rm := urlkit.NewRouteManager(
    urlkit.WithConflictPolicy(urlkit.RouteConflictPolicyReplace),
)
Freeze And Manifest
rm.Freeze()

manifest := rm.Manifest()
diff := urlkit.DiffRouteManifest(previousManifest, manifest)

fmt.Println(rm.Frozen())   // true
fmt.Println(len(diff.Added))
Group

Container for related routes with a shared base URL.

group := urlkit.NewURIHelper("https://api.example.com", map[string]string{
    "users": "/users/:id",
    "posts": "/posts/:postId",
})
Builder

Fluent API for constructing URLs with parameters and queries.

url := group.Builder("users").
    WithParam("id", "123").
    WithQuery("include", "profile").
    WithQuery("format", "json").
    MustBuild()
Advanced Builder Helpers
type userParams struct {
    Locale string `json:"locale"`
}

url := group.Builder("user").
    WithParamsMap(map[string]any{"id": 42}).
    WithStruct(userParams{Locale: "fr"}).
    WithQueryValues(map[string][]string{"tag": []string{"new", "sale"}}).
    MustBuild()
// Result: https://api.example.com/users/42/fr?tag=new&tag=sale

WithParamsMap and WithStruct normalize Go values into the builder's parameter map, merging struct fields by tag (defaulting to JSON semantics). WithQueryValues makes it easy to add multi-value query parameters using the standard map[string][]string shape.

Usage Examples

Basic Route Rendering
routes := map[string]string{
    "user":     "/users/:id",
    "userPost": "/users/:userId/posts/:postId",
}

group := urlkit.NewURIHelper("https://api.example.com", routes)

// Simple parameter substitution
url, err := group.Render("user", urlkit.Params{"id": "123"})
// Result: https://api.example.com/users/123

// Multiple parameters
url, err = group.Render("userPost", urlkit.Params{
    "userId": "123",
    "postId": "456",
})
// Result: https://api.example.com/users/123/posts/456
Adding Query Parameters
// Single query parameter
url, err := group.Render("user",
    urlkit.Params{"id": "123"},
    urlkit.Query{"include": "profile"},
)
// Result: https://api.example.com/users/123?include=profile

// Multiple query parameters
url, err := group.Render("user",
    urlkit.Params{"id": "123"},
    urlkit.Query{"include": "profile"},
    urlkit.Query{"format": "json"},
)
// Result: https://api.example.com/users/123?include=profile&format=json
Builder Pattern
builder := group.Builder("userPost")
builder.WithParam("userId", 123)
builder.WithParam("postId", 456)
builder.WithQuery("include", "comments")
builder.WithQuery("page", 2)

url, err := builder.Build()
// Result: https://api.example.com/users/123/posts/456?include=comments&page=2

// Or chain the calls
url = group.Builder("userPost").
    WithParam("userId", 123).
    WithParam("postId", 456).
    WithQuery("include", "comments").
    MustBuild() // Panics on error
Route Manager Resolver

RouteManager satisfies the Resolver interface so you can build URLs without holding onto group handles.

var resolver urlkit.Resolver = rm

url, err := resolver.Resolve("frontend", "login", urlkit.Params{}, urlkit.Query{"lang": "en"})
// Result: https://app.example.com/auth/login?lang=en

url, err = rm.ResolveWith(
    "frontend.profile",
    "details",
    map[string]any{"userId": 123},
    map[string][]string{"tab": []string{"posts", "mentions"}},
)
// Result: https://app.example.com/profile/123?tab=posts&tab=mentions
Route Manager Router Registration Helpers

RoutePath and RouteTemplate return deterministic route templates without base URLs or query strings. These are intended for router registration and inspection while Resolve/ResolveWith handle URL generation.

path, err := rm.RoutePath("admin.api", "preview")
// Result: /admin/api/preview/:token

template, err := rm.RouteTemplate("admin.api", "preview")
// Result: /preview/:token
Route Manager with Multiple Groups
rm := urlkit.NewRouteManager()

// Frontend routes
rm.RegisterGroup("frontend", "https://app.example.com", map[string]string{
    "login":    "/auth/login",
    "dashboard": "/dashboard",
    "profile":   "/profile/:userId",
})

// API routes
rm.RegisterGroup("api", "https://api.example.com/v1", map[string]string{
    "users":  "/users/:id",
    "posts":  "/posts",
})

// Webhook routes
rm.RegisterGroup("webhooks", "https://webhooks.example.com", map[string]string{
    "stripe": "/webhooks/stripe",
    "github": "/webhooks/github/:event",
})

// Build URLs from different groups
frontendURL := rm.Group("frontend").Builder("profile").
    WithParam("userId", "123").
    MustBuild()

apiURL := rm.Group("api").Builder("users").
    WithParam("id", "123").
    WithQuery("include", "posts").
    MustBuild()
Route Validation
rm := urlkit.NewRouteManager()
rm.RegisterGroup("api", "https://api.example.com", map[string]string{
    "users": "/users/:id",
    "posts": "/posts/:id",
})

// Define expected routes per group
expected := map[string][]string{
    "api": {"users", "posts", "comments"}, // "comments" is missing
}

// Validate configuration
if err := rm.Validate(expected); err != nil {
    fmt.Printf("Validation failed: %v\n", err)
    // Output: validation error: group api missing: [comments]
}

// Use MustValidate to panic on validation errors
rm.MustValidate(expected) // Will panic
Optional Parameters
// Routes with optional parameters (using ? suffix)
routes := map[string]string{
    "webhook": "/webhooks/:service/:uuid?",
}

group := urlkit.NewURIHelper("https://api.example.com", routes)

// With optional parameter
url, _ := group.Render("webhook", urlkit.Params{
    "service": "gmail",
    "uuid":    "123",
})
// Result: https://api.example.com/webhooks/gmail/123

// Without optional parameter
url, _ = group.Render("webhook", urlkit.Params{
    "service": "gmail",
})
// Result: https://api.example.com/webhooks/gmail
Template Based URL Generation

The library supports template based URL generation that provides flexible, maintainable URL structures with variable inheritance:

// Create a route manager
rm := urlkit.NewRouteManager()

// Create a group with URL template
rm.RegisterGroup("app", "https://app.example.com", map[string]string{
    "dashboard": "/dashboard",
    "profile":   "/profile/:userId",
})
app := rm.Group("app")

// Set up template with variables
app.SetURLTemplate("https://{tenant}.{domain}{route_path}")
app.SetTemplateVar("domain", "myapp.com")

// Create tenant-specific child groups
acme := app.RegisterGroup("acme", "", map[string]string{
    "dashboard": "/dashboard",
    "settings":  "/settings/:section",
})
acme.SetTemplateVar("tenant", "acme")

// Generate URLs using the template
url, _ := acme.Builder("dashboard").Build()
// Result: https://acme.myapp.com/dashboard

url, _ = acme.Builder("settings").WithParam("section", "billing").Build()
// Result: https://acme.myapp.com/settings/billing
Internationalization with Templates
// Load i18n configuration from JSON
config, err := loadConfigFromFile("i18n_config.json") // You'll need to implement this helper
if err != nil {
    log.Fatal(err)
}
rm, err := urlkit.NewRouteManagerFromConfig(&config)
if err != nil {
    log.Fatal(err)
}

// English URLs
enGroup := rm.Group("frontend.en")
aboutEN, _ := enGroup.Builder("about").Build()
// Result: https://www.example.com/en/about-us

// Spanish URLs
esGroup := rm.Group("frontend.es")
aboutES, _ := esGroup.Builder("about").Build()
// Result: https://www.example.com/es/acerca-de
Template Features
  • Variable Inheritance: Child groups inherit parent variables and can override them
  • Dynamic Variables: Automatically provided variables like route_path and base_url
  • Flexible Patterns: Support for protocol, subdomain, path, and query customization
  • JSON Configuration: Load complex template configurations from JSON files

See examples/ for comprehensive template usage examples.

Template Helpers

The library provides template helper functions that integrate seamlessly with Go template engines like go-template, enabling clean URL generation directly in templates.

Basic Setup
import (
    "github.com/goliatone/go-template"
    "github.com/goliatone/go-urlkit"
)

func main() {
    // Setup URLKit with routes
    manager := urlkit.NewRouteManager()
    manager.RegisterGroup("frontend", "https://example.com", map[string]string{
        "home":         "/",
        "user_profile": "/users/:id/profile",
        "contact":      "/contact",
    })

    // Create template renderer with URLKit helpers
    renderer, err := template.NewRenderer(
        template.WithBaseDir("./templates"),
        template.WithGlobalData(urlkit.TemplateHelpers(manager, nil)),
    )
    if err != nil {
        panic(err)
    }
}
Template Helper Functions
url(group, route, [params], [query])

Generate complete URLs with optional path parameters and query strings:

<!-- Simple URL -->
<a href="{{ url('frontend', 'home') }}">Home</a>
<!-- Result: https://example.com/ -->

<!-- With path parameters -->
<a href="{{ url('frontend', 'user_profile', {'id': user.id}) }}">Profile</a>
<!-- Result: https://example.com/users/123/profile -->

<!-- With query strings -->
<a href="{{ url('api', 'users', {}, {'page': 1, 'limit': 10}) }}">Users API</a>
<!-- Result: https://api.example.com/users?page=1&limit=10 -->

<!-- With both parameters and query strings -->
<a href="{{ url('api', 'user_posts', {'id': 1}, {'sort': 'date'}) }}">Posts</a>
<!-- Result: https://api.example.com/users/1/posts?sort=date -->

The helpers are also registered using PascalCase aliases (URL, RoutePath, Navigation, etc.) for teams that prefer Go-style naming in templates.

route_path(group, route, [params], [query])

Generate path and query portion only (for JavaScript/AJAX usage):

<script>
const apiPath = "{{ route_path('api', 'users', {'id': 123}, {'format': 'json'}) }}";
// Result: /api/users/123?format=json
</script>
has_route(group, route)

Check if a route exists for conditional rendering:

{% if has_route('admin', 'dashboard') %}
    <a href="{{ url('admin', 'dashboard') }}">Admin Panel</a>
{% endif %}
route_template(group, route)

Get the raw route template for debugging:

<!-- Shows: "/users/:id/profile" -->
<div>Route template: {{ route_template('frontend', 'user_profile') }}</div>
navigation(group, routes, [params])

Return structured navigation nodes ready for rendering menus. The optional params map lets you supply per-route parameters.

{% set main_nav = Navigation('frontend', ['home', 'profile'], {
    'profile': {'id': current_user.id},
}) %}

<nav>
  {% for item in main_nav %}
    <a href="{{ item.url }}" class="{% if item.full_route == current_route_name %}active{% endif %}">
      {{ item.route|title }}
    </a>
  {% endfor %}
</nav>
Contextual Features

Template helpers support contextual features like navigation active states and URL rebuilding by accessing template variables. These context variables are typically provided by middleware that injects routing information into your template data.

Context Variables

Your application should provide the following template variables:

  • current_route_name: Current route identifier (e.g., "frontend.user_profile")
  • current_params: Current URL path parameters (e.g., {"id": "123"})
  • current_query: Current query parameters (e.g., {"page": "2"})
Integration with go-router

If you're using go-router, the routecontext middleware automatically provides these variables. The middleware can be configured to export them either as top-level template variables or nested under a configurable map key:

// Default configuration exports as top-level variables:
// current_route_name, current_params, current_query
middleware.RouteContext(middleware.RouteContextConfig{
    ExportAsMap: false, // Export as individual template variables
})

// Or export under a template_context map:
middleware.RouteContext(middleware.RouteContextConfig{
    ExportAsMap: true,
    MapKey: "template_context", // Variables nested under {{ template_context.current_route_name }}
})
Navigation Active States
<nav>
    <a href="{{ url('frontend', 'home') }}"
       class="{{ current_route_if('frontend.home', current_route_name, 'active') }}">
        Home
    </a>
    <a href="{{ url('frontend', 'profile') }}"
       class="{{ current_route_if('frontend.profile', current_route_name, 'active', 'inactive') }}">
        Profile
    </a>
</nav>
URL Rebuilding with Modified Query Parameters
<!-- Current URL: /products?category=electronics&page=2 -->
<thead>
    <tr>
        <th>
            <a href="{{ url('frontend', current_route_name, current_params, current_query|merge({'sort': 'name'})) }}">
                Product Name
            </a>
        </th>
    </tr>
</thead>
<!-- Result: /products?category=electronics&page=2&sort=name -->
Pagination Controls
<div class="pagination">
    <a href="{{ url('frontend', current_route_name, current_params, current_query|merge({'page': current_query.page|int - 1})) }}">
        Previous
    </a>
    <a href="{{ url('frontend', current_route_name, current_params, current_query|merge({'page': current_query.page|int + 1})) }}">
        Next
    </a>
</div>
Current Route Detection
<!-- Access current route directly -->
<body data-route="{{ current_route_name }}">

<!-- Conditional content based on current route -->
{% if current_route_name == 'frontend.user_profile' %}
    <div class="profile-actions">
        <a href="{{ url('frontend', 'edit_profile', current_params) }}">Edit Profile</a>
    </div>
{% endif %}
Template Data Integration

When rendering templates, merge the contextual data with your template variables:

func renderTemplate(w http.ResponseWriter, r *http.Request, templateName string, data map[string]any) {
    // Add contextual data from your routing system
    data["current_route_name"] = "frontend.user_profile"
    data["current_params"] = map[string]any{"id": "123"}
    data["current_query"] = map[string]string{"tab": "profile"}

    // Render template
    renderer.ExecuteTemplate(w, templateName, data)
}

The template helpers automatically handle parameter conversion, error handling, and URL encoding, providing a robust foundation for template-based URL generation.

URL Joining Utility

The package also provides a standalone URL joining function:

// Basic path joining
url := urlkit.JoinURL("https://api.example.com", "/users")
// Result: https://api.example.com/users

// With query parameters
url = urlkit.JoinURL("https://api.example.com", "/users",
    urlkit.Query{"page": "1"},
    urlkit.Query{"limit": "10"},
)
// Result: https://api.example.com/users?page=1&limit=10

// Preserves existing query parameters
url = urlkit.JoinURL("https://api.example.com?existing=1", "/users",
    urlkit.Query{"new": "2"},
)
// Result: https://api.example.com/users?existing=1&new=2

OAuth2 Integration

The library includes a complete OAuth2 client with state management, encryption, and support for multiple providers. It provides a secure, type safe way to implement OAuth2 authorization flows.

Features
  • Generic Provider Interface: Support for any OAuth2 provider (Google, GitHub, Facebook, etc.)
  • State Management: Automatic CSRF protection with encrypted state parameters
  • Type-Safe User Data: Attach custom data structures to the OAuth2 flow
  • Built-in Encryption: AES encryption for sensitive state data
  • Thread-Safe: Concurrent operation support
  • Comprehensive Error Handling: Detailed error types for different failure scenarios
Quick Start
import "github.com/goliatone/go-urlkit/oauth2"

// Define custom user data for the OAuth2 flow
type UserContext struct {
    UserID   string `json:"user_id"`
    ReturnTo string `json:"return_to"`
}

// Create a Google OAuth2 provider
provider, err := oauth2.NewGoogleProvider()
if err != nil {
    log.Fatal(err)
}

// Create OAuth2 client with user context type
client, err := oauth2.NewClient[UserContext](
    provider,
    "your-google-client-id",
    "your-google-client-secret",
    "http://localhost:8080/oauth/callback",
    "your-24-character-encrypt-key",
)
if err != nil {
    log.Fatal(err)
}

// Generate authorization URL with user context
userCtx := UserContext{
    UserID:   "user123",
    ReturnTo: "/dashboard",
}
authURL, err := client.GenerateURL("random-csrf-token", userCtx)
if err != nil {
    log.Fatal(err)
}

// Redirect user to authURL...

// Handle OAuth2 callback
func handleCallback(w http.ResponseWriter, r *http.Request) {
    code := r.URL.Query().Get("code")
    state := r.URL.Query().Get("state")

    // Validate state and retrieve user context
    originalState, userCtx, err := client.ValidateState(state)
    if err != nil {
        http.Error(w, "Invalid state", http.StatusBadRequest)
        return
    }

    // Exchange code for access token
    token, err := client.Exchange(r.Context(), code)
    if err != nil {
        http.Error(w, "Token exchange failed", http.StatusInternalServerError)
        return
    }

    // Get user information
    userInfo, err := client.GetUserInfo(token)
    if err != nil {
        http.Error(w, "Failed to get user info", http.StatusInternalServerError)
        return
    }

    // Use userCtx.ReturnTo to redirect user back to their original destination
    // userInfo contains the OAuth2 user profile data
}
Extending OAuth2 Scopes
// Add additional scopes for Google services
oauth2.AddGoogleScopes(provider, []string{"gmail", "drive"})

// Or set custom scopes
provider.SetScopes([]string{
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/drive.file",
})
Custom OAuth2 Providers
// Implement the Provider interface for custom OAuth2 providers
type CustomProvider struct {
    name      string
    config    *oauth2.Config
    scopes    []string
    userURL   string
}

func (p *CustomProvider) Name() string { return p.name }
func (p *CustomProvider) Config() *oauth2.Config { return p.config }
func (p *CustomProvider) Scopes() []string { return p.scopes }
func (p *CustomProvider) SetScopes(scopes []string) { p.scopes = scopes }
func (p *CustomProvider) GetUserInfo(token *oauth2.Token) (map[string]any, error) {
    // Implementation for fetching user info from your provider
}

// Use your custom provider
provider := &CustomProvider{
    name: "mycorp",
    config: &oauth2.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://auth.mycorp.com/oauth/authorize",
            TokenURL: "https://auth.mycorp.com/oauth/token",
        },
    },
    userURL: "https://api.mycorp.com/user",
}

client, err := oauth2.NewClient[UserContext](provider, ...)
OAuth2 Error Handling
// State validation errors
_, userCtx, err := client.ValidateState(state)
if err != nil {
    switch {
    case errors.Is(err, oauth2.ErrStateNotFound):
        // CSRF attack or expired state
    case errors.Is(err, oauth2.ErrDecryptionFailed):
        // Encryption key mismatch or corrupted data
    case errors.Is(err, oauth2.ErrDeserializationFailed):
        // JSON parsing failed for user data
    }
}
OAuth2 Examples

See examples/oauth2_example.go and examples/oauth2/ for comprehensive OAuth2 integration examples including:

  • Complete authorization flow implementation
  • State management and validation
  • Error handling for different scenarios
  • Multi provider support
  • User data preservation across the OAuth2 flow

Error Handling

The library provides specific error types for different failure scenarios:

ValidationError

Returned when route validation fails:

if err := rm.Validate(expected); err != nil {
    if ve, ok := err.(urlkit.ValidationError); ok {
        for group, missing := range ve.Errors {
            fmt.Printf("Group %s is missing routes: %v\n", group, missing)
        }
    }
}
GroupValidationError

Returned when a specific group fails validation:

if err := group.Validate([]string{"users", "posts"}); err != nil {
    if gve, ok := err.(urlkit.GroupValidationError); ok {
        fmt.Printf("Missing routes: %v\n", gve.MissingRoutes)
    }
}

Testing

Run tests with:

./taskfile dev:test

Run tests with coverage:

./taskfile dev:cover

Requirements

  • Go 1.23.4 or later
  • github.com/soongo/path-to-regexp v1.6.4
  • golang.org/x/oauth2 (for OAuth2 functionality)
  • github.com/google/uuid (for OAuth2 functionality)

License

See LICENSE file for details.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrGroupNotFound = errors.New("group not found")
	ErrRouteNotFound = errors.New("route not found")
)

Functions

func JoinURL

func JoinURL(base, path string, queries ...Query) string

func SubstituteTemplate

func SubstituteTemplate(template string, vars map[string]string) string

SubstituteTemplate performs string substitution on URL templates using the specified variable map, implementing the {variable_name} placeholder syntax.

Template Syntax:

  • Placeholders use curly brace notation: {variable_name}
  • Variable names are case-sensitive and can contain letters, numbers, and underscores
  • Nested braces are not supported: {{variable}} is treated as literal text
  • Missing variables: If a placeholder's variable is not found in the vars map, the placeholder is left unchanged in the output string

Supported Placeholder Examples:

  • {protocol} → "https" (if vars["protocol"] = "https")
  • {host} → "example.com" (if vars["host"] = "example.com")
  • {route_path} → "/about" (built-in dynamic variable)
  • {missing} → "{missing}" (unchanged if not in vars map)

Parameters:

  • template: The template string containing {variable} placeholders
  • vars: Map of variable names to their string values for substitution

Returns:

  • string: The template with all found variables substituted, unfound placeholders remain unchanged for debugging purposes

Example:

SubstituteTemplate("{proto}://{host}/{path}", map[string]string{
  "proto": "https", "host": "api.example.com", "path": "v1"
})
Returns: "https://api.example.com/v1"

func TemplateHelpers

func TemplateHelpers(manager *RouteManager, config *TemplateHelperConfig) map[string]any

TemplateHelpers returns a map of template helper functions for use with template engines

func TemplateHelpersWithLocale

func TemplateHelpersWithLocale(manager *RouteManager, config *TemplateHelperConfig, localeConfig *LocaleConfig) map[string]any

TemplateHelpersWithLocale returns a map of template helper functions with localization support The returned map can be passed to template.WithTemplateFunc() during engine initialization.

Usage:

manager := NewRouteManager()
config := DefaultTemplateHelperConfig()
localeConfig := DefaultLocaleConfig()
localeConfig.SupportedLocales = []string{"en", "es", "fr"}
localeConfig.LocaleGroups["frontend"] = []string{"en", "es"}
renderer, err := template.NewRenderer(
    template.WithTemplateFunc(urlkit.TemplateHelpersWithLocale(manager, config, localeConfig)),
)

Template usage:

{{ url_i18n('frontend', 'user_profile', {'id': user.id}) }}
{{ url_locale('frontend', 'about', 'es') }}

Types

type Builder

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

func (*Builder) Build

func (b *Builder) Build() (string, error)

func (*Builder) MustBuild

func (b *Builder) MustBuild() string

func (*Builder) WithParam

func (b *Builder) WithParam(key string, value any) *Builder

func (*Builder) WithParamsMap added in v0.2.0

func (b *Builder) WithParamsMap(values map[string]any) *Builder

func (*Builder) WithQuery

func (b *Builder) WithQuery(key string, value any) *Builder

func (*Builder) WithQueryValues added in v0.2.0

func (b *Builder) WithQueryValues(values map[string][]string) *Builder

func (*Builder) WithStruct added in v0.2.0

func (b *Builder) WithStruct(value any) *Builder

type Config

type Config struct {
	Groups []GroupConfig `json:"groups" yaml:"groups"`
}

func (Config) GetGroups

func (c Config) GetGroups() []GroupConfig

GetGroups implements the Configurator interface for the Config struct.

type Configurator

type Configurator interface {
	GetGroups() []GroupConfig
}

Configurator defines the interface for route manager configuration. This interface follows the Config Getters pattern and allows for flexible configuration implementations that can be generated automatically.

type FrozenRouteManagerError added in v0.6.0

type FrozenRouteManagerError struct {
	Operation string
	GroupFQN  string
}

func (FrozenRouteManagerError) Error added in v0.6.0

func (e FrozenRouteManagerError) Error() string

type Group

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

Group represents a collection of routes with optional templating capabilities. Groups can be organized in a hierarchy where child groups inherit and can override template variables from their parents.

Template System: Groups support two URL generation modes: 1. Path Concatenation (default): URLs are built by concatenating baseURL + group paths + route 2. Template Rendering: URLs are built using a template string with variable substitution

Template Variable Precedence (highest to lowest priority): - Built in variables (base_url, route_path) - Current group's template variables - Parent group's template variables (recursively up the hierarchy)

Supported Template Syntax: - {variable_name}: Substitutes the variable with its value - {base_url}: Automatically available, contains the root group's base URL - {route_path}: Automatically available, contains the compiled route with parameters

Example (ApiVersioningPattern)

ExampleGroup_apiVersioningPattern demonstrates how to use nested groups for API versioning with backward compatibility.

// Create a route manager for a versioned API
rm := urlkit.NewRouteManager()

// Register the main API group
rm.RegisterGroup("api", "https://api.myservice.com", map[string]string{
	"status": "/status",
	"health": "/health",
})

api := rm.Group("api")

// Add v1 API routes
v1, _, err := api.RegisterGroup("v1", "/v1", map[string]string{
	"users":    "/users/:id",
	"posts":    "/posts",
	"comments": "/posts/:postId/comments",
})
if err != nil {
	panic(err)
}

// Add v2 API routes with new endpoints
api.RegisterGroup("v2", "/v2", map[string]string{
	"users":    "/users/:id",
	"profiles": "/users/:id/profile",
	"posts":    "/posts/:id",
	"teams":    "/teams/:teamId",
})

// Add admin endpoints to v1
v1.RegisterGroup("admin", "/admin", map[string]string{
	"dashboard": "/dashboard",
	"settings":  "/settings/:section",
	"reports":   "/reports/:type/:date?",
})

// Build URLs for different API versions
// Root API status: https://api.myservice.com/status
statusURL, _ := rm.Group("api").Builder("status").Build()
fmt.Println("API Status URL:", statusURL)

// V1 user endpoint: https://api.myservice.com/v1/users/123
v1UserURL, _ := rm.Group("api").Group("v1").Builder("users").
	WithParam("id", "123").
	Build()
fmt.Println("V1 User URL:", v1UserURL)

// V2 user profile: https://api.myservice.com/v2/users/123/profile
v2ProfileURL, _ := rm.Group("api").Group("v2").Builder("profiles").
	WithParam("id", "123").
	Build()
fmt.Println("V2 Profile URL:", v2ProfileURL)

// V1 admin dashboard: https://api.myservice.com/v1/admin/dashboard
adminURL, _ := rm.Group("api").Group("v1").Group("admin").Builder("dashboard").Build()
fmt.Println("Admin Dashboard URL:", adminURL)

// V1 admin reports with optional date: https://api.myservice.com/v1/admin/reports/sales/2023-12
reportsURL, _ := rm.Group("api").Group("v1").Group("admin").Builder("reports").
	WithParam("type", "sales").
	WithParam("date", "2023-12").
	Build()
fmt.Println("Admin Reports URL:", reportsURL)
Output:

API Status URL: https://api.myservice.com/status
V1 User URL: https://api.myservice.com/v1/users/123
V2 Profile URL: https://api.myservice.com/v2/users/123/profile
Admin Dashboard URL: https://api.myservice.com/v1/admin/dashboard
Admin Reports URL: https://api.myservice.com/v1/admin/reports/sales/2023-12
Example (InternationalizationPattern)

ExampleGroup_internationalizationPattern demonstrates how to use nested groups for internationalization with localized routes and content.

// Create a route manager for an internationalized website
rm := urlkit.NewRouteManager()

// Register the main frontend group
rm.RegisterGroup("frontend", "https://mywebsite.com", map[string]string{
	"home": "/",
})

frontend := rm.Group("frontend")

// Add English locale routes
frontend.RegisterGroup("en", "/en", map[string]string{
	"about":    "/about-us",
	"contact":  "/contact",
	"products": "/products/:category",
})

// Add Spanish locale routes with localized paths
frontend.RegisterGroup("es", "/es", map[string]string{
	"about":    "/acerca-de",
	"contact":  "/contacto",
	"products": "/productos/:category",
})

// Build URLs for different locales
// English: https://mywebsite.com/en/about-us
enAboutURL, _ := rm.Group("frontend").Group("en").Builder("about").Build()
fmt.Println("English About URL:", enAboutURL)

// Spanish: https://mywebsite.com/es/acerca-de
esAboutURL, _ := rm.Group("frontend").Group("es").Builder("about").Build()
fmt.Println("Spanish About URL:", esAboutURL)

// English Products with category: https://mywebsite.com/en/products/electronics
enProductsURL, _ := rm.Group("frontend").Group("en").Builder("products").
	WithParam("category", "electronics").
	Build()
fmt.Println("English Products URL:", enProductsURL)

// Spanish Products with category: https://mywebsite.com/es/productos/electronics
esProductsURL, _ := rm.Group("frontend").Group("es").Builder("products").
	WithParam("category", "electronics").
	Build()
fmt.Println("Spanish Products URL:", esProductsURL)
Output:

English About URL: https://mywebsite.com/en/about-us
Spanish About URL: https://mywebsite.com/es/acerca-de
English Products URL: https://mywebsite.com/en/products/electronics
Spanish Products URL: https://mywebsite.com/es/productos/electronics

func NewURIHelper

func NewURIHelper(baseURL string, routes map[string]string) *Group

func (*Group) AddRoutes

func (u *Group) AddRoutes(routes map[string]string) (RouteMutationResult, error)

AddRoutes dynamically adds new routes to this group at runtime. Routes are immediately compiled and available for URL building. Existing routes with the same name are replaced and recompiled. This method is useful for conditional route registration or dynamic route generation based on configuration.

Parameters:

  • routes: a map of route names to path templates (e.g., "users": "/users/:id")

Path templates follow the same syntax as route registration:

  • Static segments: "/users/profile"
  • Parameters: "/users/:id" or "/posts/:postId/comments/:commentId"
  • Optional parameters: "/search/:query?"

Example:

group.AddRoutes(map[string]string{
    "webhooks": "/webhooks/:event",
    "status":   "/status",
})

func (*Group) Builder

func (u *Group) Builder(routeName string) *Builder

func (*Group) CollectTemplateVars

func (u *Group) CollectTemplateVars() map[string]string

CollectTemplateVars aggregates template variables from the entire group hierarchy, implementing a child-overrides-parent precedence system.

The collection process starts from the root group and moves down to the current group, ensuring that variables defined in child groups override those with the same key defined in parent groups. This allows for flexible variable inheritance and specialization at different hierarchy levels.

Variable Precedence Rules (highest to lowest priority):

  1. Built in dynamic variables (route_path, base_url)
  2. Current group's templateVars
  3. Parent groups' templateVars (closer ancestors override distant ones)

Returns:

  • map[string]string: A merged map of all template variables with proper precedence applied. Keys are variable names, values are their string values.

Example:

If parent has {"lang": "en", "theme": "light"} and child has {"lang": "es"},
the result will be {"lang": "es", "theme": "light"}.

func (*Group) FQN added in v0.3.0

func (u *Group) FQN() string

FQN returns the group's fully qualified name within the hierarchy (dot notation). Root groups return their own name, while nested groups include their ancestors (e.g., "frontend.en.marketing"). An empty string indicates the group is detached from the manager hierarchy.

func (*Group) FindTemplateOwner

func (u *Group) FindTemplateOwner() *Group

FindTemplateOwner traverses up the group hierarchy to locate the first ancestor group (including the current group) that defines a URL template.

The method performs a depth-first search starting from the current group and moving up the parent chain until it finds a group with a non-empty urlTemplate field.

Returns:

  • *Group: The group that owns the URL template, or nil if no template is found in the entire hierarchy chain.

This method is essential for template-based URL construction as it determines which group's template should be used for rendering the final URL.

func (*Group) GetTemplateVar

func (u *Group) GetTemplateVar(key string) (string, bool)

GetTemplateVar retrieves a template variable value from this group's local variables only. This method does NOT search the hierarchy - it only returns variables directly set on this group. Use CollectTemplateVars() to get the complete set of variables with inheritance applied.

Returns:

  • value: the variable value if found
  • exists: true if the variable exists in this group's local variables

Example:

value, exists := group.GetTemplateVar("locale")
if exists {
    fmt.Printf("Locale is set to: %s\n", value)
}

func (*Group) Group

func (u *Group) Group(name string) *Group

Group returns a child group by name for fluent API traversal. It panics if the child group is not found.

func (*Group) MustRoute

func (u *Group) MustRoute(routeName string) string

func (*Group) Navigation added in v0.2.0

func (u *Group) Navigation(routes []string, params func(route string) Params) ([]NavigationNode, error)

Navigation builds a slice of NavigationNode entries for the provided routes. The params callback can supply per-route parameter maps which are applied before building URLs.

func (*Group) RegisterGroup

func (u *Group) RegisterGroup(name, path string, routes map[string]string) (*Group, RouteMutationResult, error)

RegisterGroup creates and registers a new child group under the current group.

func (*Group) Render

func (u *Group) Render(routeName string, params Params, queries ...Query) (string, error)

func (*Group) Route

func (u *Group) Route(routeName string) (string, error)

func (*Group) SetTemplateVar

func (u *Group) SetTemplateVar(key, value string) error

SetTemplateVar sets a template variable that will be available for substitution in URL templates. Template variables follow a hierarchical inheritance pattern where child groups can override parent variables.

Variable Precedence (highest to lowest priority):

  1. Built in variables (base_url, route_path) - cannot be overridden
  2. Current group's variables
  3. Parent group's variables (recursively up the hierarchy)

Common use cases:

  • SetTemplateVar("locale", "en-US") for internationalization
  • SetTemplateVar("version", "v2") for API versioning
  • SetTemplateVar("env", "staging") for environment-specific URLs
  • SetTemplateVar("region", "eu-west") for regional deployments

func (*Group) SetURLTemplate

func (u *Group) SetURLTemplate(template string) error

SetURLTemplate sets the URL template string for this group, enabling template-based URL generation. When a template is set, this group becomes a "template owner" and all URL generation for this group and its descendants will use template rendering instead of path concatenation.

Template Syntax:

  • Use {variable_name} to insert template variables
  • {base_url} is automatically available (the root group's base URL)
  • {route_path} is automatically available (the compiled route with parameters)

Example templates:

  • "{base_url}/api/{version}{route_path}"
  • "{protocol}://{host}/{locale}/{section}{route_path}"
  • "{base_url}/{env}/{service}{route_path}"

To disable template rendering and revert to path concatenation, pass an empty string.

func (*Group) Validate

func (u *Group) Validate(routes []string) error

Validate checks whether the group contains all expected routes. It returns a GroupValidationError if any routes are missing.

type GroupCache

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

GroupCache provides simple caching for frequently accessed groups This reduces the overhead of repeated group lookups in template helpers

func NewGroupCache

func NewGroupCache(manager *RouteManager) *GroupCache

NewGroupCache creates a new group cache

func (*GroupCache) Clear

func (gc *GroupCache) Clear()

Clear clears the cache (useful for testing or when routes change)

func (*GroupCache) Get

func (gc *GroupCache) Get(groupName string) *Group

Get retrieves a group from cache or fetches it from the manager

type GroupConfig

type GroupConfig struct {
	Name    string            `json:"name" yaml:"name"`
	BaseURL string            `json:"base_url,omitempty" yaml:"base_url,omitempty"`
	Path    string            `json:"path,omitempty" yaml:"path,omitempty"`
	Routes  map[string]string `json:"routes,omitempty" yaml:"routes,omitempty"`
	Paths   map[string]string `json:"paths,omitempty" yaml:"paths,omitempty"` // legacy support
	Groups  []GroupConfig     `json:"groups,omitempty" yaml:"groups,omitempty"`

	// URLTemplate defines the URL structure using placeholder syntax.
	// Example: "{protocol}://{host}/{locale}/{section}{route_path}"
	// When set, this group becomes a template owner and uses template rendering
	// instead of simple path concatenation. Template variables are substituted
	// using {variable_name} syntax.
	URLTemplate string `json:"url_template,omitempty" yaml:"url_template,omitempty"`

	// TemplateVars contains key-value pairs that this group contributes to template rendering.
	// Child groups can override parent variables, following a precedence rule where
	// child variables take priority over parent variables.
	// Special variables:
	//   - base_url: Automatically set to the group's base URL
	//   - route_path: Automatically set to the compiled route path with parameters
	TemplateVars map[string]string `json:"template_vars,omitempty" yaml:"template_vars,omitempty"`
}

GroupConfig defines the configuration structure for a group when loading from JSON/YAML. It supports both traditional path concatenation and template based URL generation.

type GroupValidationError

type GroupValidationError struct {
	MissingRoutes []string
}

func (GroupValidationError) Error

func (g GroupValidationError) Error() string

type LocaleConfig

type LocaleConfig struct {
	// Default locale to use when no locale can be detected
	DefaultLocale string

	// List of supported locales (e.g., ["en", "es", "fr"])
	SupportedLocales []string

	// Map of group names to their supported locales
	// Allows fine-grained control over which locales are available for which groups
	// e.g., "frontend" -> ["en", "es"], "api" -> ["en", "fr", "de"]
	LocaleGroups map[string][]string

	// Custom locale detection function (legacy, for backward compatibility)
	// Takes template context data and returns detected locale
	LocaleDetector func(context any) string

	// Locale detection strategies in priority order
	// Uses the new multi-strategy detection system
	DetectionStrategies []LocaleDetectionStrategy

	// Locale fallback strategy
	// When true, falls back to DefaultLocale if detected locale is not supported
	// When false, returns error for unsupported locales
	EnableLocaleFallback bool

	// Hierarchical locale group structure
	// When true, supports URLKit's hierarchical groups for locale organization
	// e.g., frontend.en.help, frontend.es.help
	EnableHierarchicalLocales bool

	// Locale validation options
	EnableLocaleValidation bool // Validate detected locales against supported list
}

LocaleConfig defines configuration for localization helpers

func DefaultLocaleConfig

func DefaultLocaleConfig() *LocaleConfig

DefaultLocaleConfig returns default locale configuration

func NewFullStackLocaleConfig

func NewFullStackLocaleConfig(defaultLocale string, supportedLocales []string) *LocaleConfig

NewFullStackLocaleConfig creates a comprehensive LocaleConfig with all detection strategies

func NewHeaderBasedLocaleConfig

func NewHeaderBasedLocaleConfig(defaultLocale string, supportedLocales []string) *LocaleConfig

NewHeaderBasedLocaleConfig creates a LocaleConfig optimized for Accept-Language header detection

func NewMultiStrategyLocaleConfig

func NewMultiStrategyLocaleConfig(defaultLocale string, supportedLocales []string, strategies []LocaleDetectionStrategy) *LocaleConfig

NewMultiStrategyLocaleConfig creates a LocaleConfig with multiple detection strategies

func NewURLBasedLocaleConfig

func NewURLBasedLocaleConfig(defaultLocale string, supportedLocales []string) *LocaleConfig

NewURLBasedLocaleConfig creates a LocaleConfig optimized for URL-based locale detection

func (*LocaleConfig) ValidateLocaleConfig

func (c *LocaleConfig) ValidateLocaleConfig() error

ValidateLocaleConfig validates the locale configuration

type LocaleDetectionContext

type LocaleDetectionContext struct {
	// TemplateContext is the template data passed to rendering
	TemplateContext map[string]any
	// URLPath is the current request URL path for URL-based detection
	URLPath string
	// AcceptLanguage is the Accept-Language header value
	AcceptLanguage string
	// CookieLocale is the locale from cookie
	CookieLocale string
	// DefaultLocale is the fallback locale
	DefaultLocale string
}

LocaleDetectionContext provides context for locale detection

type LocaleDetectionStrategy

type LocaleDetectionStrategy int

LocaleDetectionStrategy defines different locale detection strategies

const (
	// LocaleFromContext extracts locale from template context data
	LocaleFromContext LocaleDetectionStrategy = iota
	// LocaleFromURL parses locale from URL path (e.g., /en/path)
	LocaleFromURL
	// LocaleFromHeader extracts from Accept-Language header
	LocaleFromHeader
	// LocaleFromCookie extracts from locale cookie
	LocaleFromCookie
)

type LocaleInfo

type LocaleInfo struct {
	Locale string `json:"locale"`
	URL    string `json:"url"`
}

LocaleInfo represents locale information for template helpers

type NavigationNode struct {
	Group     string `json:"group"`      // Dot-qualified group name (e.g., "frontend.en")
	Route     string `json:"route"`      // Route identifier within the group (e.g., "about")
	FullRoute string `json:"full_route"` // Fully qualified route name (e.g., "frontend.en.about")
	Path      string `json:"path"`       // Raw route template (e.g., "/about" or "/users/:id")
	URL       string `json:"url"`        // Resolved URL including host/base path
	Params    Params `json:"params,omitempty"`
}

NavigationNode represents a prebuilt navigation entry constructed from a group route. It captures enough information for templates to render menus without recomputing URLs.

type Option added in v0.6.0

type Option func(*RouteManager)

func WithConflictPolicy added in v0.6.0

func WithConflictPolicy(policy RouteConflictPolicy) Option

type Params

type Params = map[string]any

type Query

type Query map[string]string

type Resolver added in v0.2.0

type Resolver interface {
	Resolve(groupPath, route string, params Params, query Query) (string, error)
}

type RootGroupConflictError added in v0.6.0

type RootGroupConflictError struct {
	GroupName       string
	ExistingBaseURL string
	IncomingBaseURL string
}

func (RootGroupConflictError) Error added in v0.6.0

func (e RootGroupConflictError) Error() string

type RouteConflictError added in v0.6.0

type RouteConflictError struct {
	GroupFQN         string
	RouteKey         string
	ExistingTemplate string
	IncomingTemplate string
}

func (RouteConflictError) Error added in v0.6.0

func (e RouteConflictError) Error() string

type RouteConflictErrors added in v0.6.0

type RouteConflictErrors struct {
	Conflicts []RouteConflictError
}

func (RouteConflictErrors) Error added in v0.6.0

func (e RouteConflictErrors) Error() string

type RouteConflictPolicy added in v0.6.0

type RouteConflictPolicy string
const (
	RouteConflictPolicyError   RouteConflictPolicy = "error"
	RouteConflictPolicyReplace RouteConflictPolicy = "replace"
	RouteConflictPolicySkip    RouteConflictPolicy = "skip"
)

type RouteManager

type RouteManager struct {
	// contains filtered or unexported fields
}
Example (ConfigurationBasedSetup)

ExampleRouteManager_configurationBasedSetup demonstrates how to load nested groups from JSON configuration for complex application structures.

// Define a comprehensive configuration with nested groups
config := urlkit.Config{
	Groups: []urlkit.GroupConfig{
		{
			Name:    "frontend",
			BaseURL: "https://myapp.com",
			Routes: map[string]string{
				"home": "/",
			},
			Groups: []urlkit.GroupConfig{
				{
					Name: "en",
					Path: "/en",
					Routes: map[string]string{
						"about":   "/about",
						"contact": "/contact",
					},
					Groups: []urlkit.GroupConfig{
						{
							Name: "help",
							Path: "/help",
							Routes: map[string]string{
								"faq":     "/faq",
								"support": "/support/:ticketId?",
							},
						},
					},
				},
			},
		},
		{
			Name:    "api",
			BaseURL: "https://api.myapp.com",
			Routes: map[string]string{
				"status": "/status",
			},
			Groups: []urlkit.GroupConfig{
				{
					Name: "v1",
					Path: "/v1",
					Routes: map[string]string{
						"users": "/users/:id",
					},
				},
			},
		},
	},
}

// Create route manager from configuration
manager, err := urlkit.NewRouteManagerFromConfig(&config)
if err != nil {
	fmt.Println("failed to load config:", err)
	return
}

// Build URLs using the configured nested structure
// Frontend home: https://myapp.com/
homeURL, _ := manager.Group("frontend").Builder("home").Build()
fmt.Println("Home URL:", homeURL)

// English contact page: https://myapp.com/en/contact
contactURL, _ := manager.Group("frontend").Group("en").Builder("contact").Build()
fmt.Println("Contact URL:", contactURL)

// English help FAQ: https://myapp.com/en/help/faq
faqURL, _ := manager.Group("frontend").Group("en").Group("help").Builder("faq").Build()
fmt.Println("FAQ URL:", faqURL)

// English help support with ticket ID: https://myapp.com/en/help/support/T-12345
supportURL, _ := manager.Group("frontend").Group("en").Group("help").Builder("support").
	WithParam("ticketId", "T-12345").
	Build()
fmt.Println("Support URL:", supportURL)

// API status: https://api.myapp.com/status
apiStatusURL, _ := manager.Group("api").Builder("status").Build()
fmt.Println("API Status URL:", apiStatusURL)

// API v1 users: https://api.myapp.com/v1/users/user-123
usersURL, _ := manager.Group("api").Group("v1").Builder("users").
	WithParam("id", "user-123").
	WithQuery("include", "profile").
	Build()
fmt.Println("API Users URL:", usersURL)
Output:

Home URL: https://myapp.com/
Contact URL: https://myapp.com/en/contact
FAQ URL: https://myapp.com/en/help/faq
Support URL: https://myapp.com/en/help/support/T-12345
API Status URL: https://api.myapp.com/status
API Users URL: https://api.myapp.com/v1/users/user-123?include=profile
Example (ValidationWithDotSeparatedRoutes)

ExampleRouteManager_validationWithDotSeparatedRoutes demonstrates how to validate nested group configurations using dot-separated path notation.

// Create a complex nested structure
rm := urlkit.NewRouteManager()

// Register main groups
rm.RegisterGroup("frontend", "https://example.com", map[string]string{
	"home": "/",
})
rm.RegisterGroup("api", "https://api.example.com", map[string]string{
	"status": "/status",
})

// Add nested groups
frontend := rm.Group("frontend")
frontend.RegisterGroup("en", "/en", map[string]string{
	"about":   "/about",
	"contact": "/contact",
})

api := rm.Group("api")
v1, _, err := api.RegisterGroup("v1", "/v1", map[string]string{
	"users": "/users/:id",
	"posts": "/posts",
})
if err != nil {
	panic(err)
}
v1.RegisterGroup("admin", "/admin", map[string]string{
	"dashboard": "/dashboard",
})

// Define expected routes using dot-separated paths for nested groups
expectedRoutes := map[string][]string{
	"frontend":     {"home"},
	"frontend.en":  {"about", "contact"},
	"api":          {"status"},
	"api.v1":       {"users", "posts"},
	"api.v1.admin": {"dashboard"},
}

// Validate the configuration
err = rm.Validate(expectedRoutes)
if err != nil {
	fmt.Printf("Validation failed: %v\n", err)
	return
}

fmt.Println("✓ All nested groups and routes are properly configured")

// Example of validation failure - missing route
invalidExpected := map[string][]string{
	"frontend.en":  {"about", "contact", "missing-route"},
	"api.v1.admin": {"dashboard", "settings"}, // settings route doesn't exist
	"api.v2":       {"users"},                 // v2 group doesn't exist
}

err = rm.Validate(invalidExpected)
if err != nil {
	// Note: Error order may vary due to map iteration
	fmt.Println("Expected validation failure occurred (order may vary):", strings.Contains(err.Error(), "validation error"))
}
Output:

✓ All nested groups and routes are properly configured
Expected validation failure occurred (order may vary): true

func NewRouteManager

func NewRouteManager(opts ...Option) *RouteManager

func NewRouteManagerFromConfig

func NewRouteManagerFromConfig(config Configurator, opts ...Option) (*RouteManager, error)

NewRouteManagerFromConfig creates a new RouteManager from a Configurator and validates the hierarchy during construction.

func (*RouteManager) AddRoutes added in v0.2.0

func (m *RouteManager) AddRoutes(path string, routes map[string]string) (*Group, RouteMutationResult, error)

AddRoutes attaches additional routes to an existing group identified by the provided path. Routes in the map are compiled immediately and overwrite any existing routes with the same name. The path supports dot-notation for nested groups.

func (*RouteManager) DebugTree added in v0.2.0

func (m *RouteManager) DebugTree() string

DebugTree returns a formatted string representing the entire group hierarchy, including routes, templates, and effective template variables for each group. Output is stable (sorted alphabetically) to simplify inspection and diffing.

func (*RouteManager) EnsureGroup added in v0.2.0

func (m *RouteManager) EnsureGroup(path string) (*Group, error)

EnsureGroup ensures that the full group path exists, creating intermediate groups as needed. The path must start with an existing root group name. Intermediate segments can optionally define a custom path using the syntax "name:/custom-path". Missing segments default to "/name". Returns the final group or an ErrGroupNotFound if the root group does not exist.

func (*RouteManager) Freeze added in v0.6.0

func (m *RouteManager) Freeze()

func (*RouteManager) Frozen added in v0.6.0

func (m *RouteManager) Frozen() bool

func (*RouteManager) GetGroup added in v0.2.0

func (m *RouteManager) GetGroup(path string) (*Group, error)

GetGroup returns the group registered at the given path. The path may reference nested groups using dot-notation (e.g., "frontend.en.marketing"). Returns ErrGroupNotFound when the requested group does not exist.

func (*RouteManager) Group

func (m *RouteManager) Group(path string) *Group

func (*RouteManager) Manifest added in v0.6.0

func (m *RouteManager) Manifest() []RouteManifestEntry

func (*RouteManager) MustValidate

func (m *RouteManager) MustValidate(groups map[string][]string) *RouteManager

MustValidate calls Validate and panics if validation errors are found.

func (*RouteManager) RegisterGroup

func (m *RouteManager) RegisterGroup(name, baseURL string, routes map[string]string) (*Group, RouteMutationResult, error)

func (*RouteManager) Resolve added in v0.2.0

func (m *RouteManager) Resolve(groupPath, route string, params Params, query Query) (string, error)

func (*RouteManager) ResolveWith added in v0.2.0

func (m *RouteManager) ResolveWith(groupPath, route string, params any, query any) (string, error)

func (*RouteManager) RoutePath added in v0.4.0

func (m *RouteManager) RoutePath(groupPath, route string) (string, error)

RoutePath returns the full group path plus the raw route template. It excludes the base URL and does not append query parameters.

func (*RouteManager) RouteTemplate added in v0.4.0

func (m *RouteManager) RouteTemplate(groupPath, route string) (string, error)

RouteTemplate returns the raw route template for the group. It does not include the group path prefix, base URL, or query parameters.

func (*RouteManager) Validate

func (m *RouteManager) Validate(groups map[string][]string) error

Validate iterates over the given groups and their expected routes, calling each group's Validate method. It returns a ValidationError if any group is missing routes or if a group is entirely missing. Supports dot-separated group paths (e.g., "frontend.en.deep") for nested groups.

type RouteManifestChange added in v0.6.0

type RouteManifestChange struct {
	Before RouteManifestEntry
	After  RouteManifestEntry
}

type RouteManifestDiff added in v0.6.0

type RouteManifestDiff struct {
	Added   []RouteManifestEntry
	Removed []RouteManifestEntry
	Changed []RouteManifestChange
}

func DiffRouteManifest added in v0.6.0

func DiffRouteManifest(before, after []RouteManifestEntry) RouteManifestDiff

type RouteManifestEntry added in v0.6.0

type RouteManifestEntry struct {
	GroupFQN         string
	RouteKey         string
	RouteTemplate    string
	FullPathTemplate string
}

type RouteMutationResult added in v0.6.0

type RouteMutationResult struct {
	Added     []string
	Replaced  []string
	Skipped   []string
	Conflicts []RouteConflictError
}

type TemplateError

type TemplateError struct {
	Helper  string         `json:"helper"`
	Type    string         `json:"type"`
	Message string         `json:"message"`
	Context map[string]any `json:"context,omitempty"`
}

TemplateError represents structured error information for template helpers

type TemplateHelperConfig

type TemplateHelperConfig struct {
	// Error reporting configuration
	EnableStructuredErrors bool // When true, returns JSON error objects instead of simple strings
	EnableErrorLogging     bool // When true, logs errors for production debugging
}

TemplateHelperConfig defines configuration for template helpers

func DefaultTemplateHelperConfig

func DefaultTemplateHelperConfig() *TemplateHelperConfig

DefaultTemplateHelperConfig returns default configuration

type TemplateSubstitutionError added in v0.2.0

type TemplateSubstitutionError struct {
	Group         string
	Route         string
	TemplateOwner string
	Template      string
	Missing       []string
}

TemplateSubstitutionError represents a failure to replace all placeholders in a template.

func (TemplateSubstitutionError) Error added in v0.2.0

type ValidationError

type ValidationError struct {
	Errors map[string][]string
}

func (ValidationError) Error

func (v ValidationError) Error() string

Directories

Path Synopsis
examples
oauth2 command
simple_template command
templated command
Package securelink provides secure, expiring URL generation and validation using JWT tokens.
Package securelink provides secure, expiring URL generation and validation using JWT tokens.

Jump to

Keyboard shortcuts

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