roamer

package module
v1.16.1 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2025 License: MIT Imports: 13 Imported by: 0

README

roamer

Go Report Card Build Status Coverage Status Go Reference Go Version GitHub release Mentioned in Awesome Go

Roamer is a flexible, extensible HTTP request parser for Go that makes handling and extracting data from HTTP requests effortless. It provides a declarative way to map HTTP request data to Go structs using struct tags.

graph TD
    subgraph "Input"
        A[HTTP Request]
    end

    subgraph "Data Sources"
        B1[Headers]
        B2[Cookies]
        B3[Query Params]
        B4[Path Variables]
        B5[Request Body]
        B6[Custom]
    end

    subgraph "Roamer Core Engine"
        direction LR
        P[Parsers]
        D[Decoders]
        F[Formatters]
    end

    subgraph "Output"
        E[Populated Go Struct]
    end

    A --> B1 & B2 & B3 & B4 & B5 & B6

    B1 & B2 & B3 & B4 & B6 -- values for --> P
    B5 -- content for --> D

    P -- parsed data --> F
    D -- decoded data --> F

    F -- formatted values --> E

    classDef source stroke:#d4ac0d,stroke-width:4px
    classDef core stroke:#0097c0,stroke-width:4px
    classDef io stroke:#333,stroke-width:4px
    class A,E io
    class B1,B2,B3,B4,B5,B6 source
    class P,D,F core

Features

  • Multiple data sources: Parse data from HTTP headers, cookies, query parameters, path variables, and request body
  • Content-type based decoding: Automatically decode JSON, XML, form data, and multipart forms
  • Default Values: Set default values for fields using the default tag
  • Formatters: Transform parsed data (trim strings, apply numeric constraints, handle time zones, manipulate slices)
  • Router integration: Built-in support for Chi, Gorilla Mux, and HttpRouter
  • Type conversion: Automatic conversion of string values to appropriate Go types
  • Extensibility: Easily create custom parsers, decoders, and formatters
  • Middleware support: Convenient middleware for integrating with HTTP handlers
  • Body preservation: Read request body multiple times when needed

Installation

go get -u github.com/slipros/roamer@latest

For router integrations:

# Chi router
go get -u github.com/slipros/roamer/pkg/chi@latest

# Gorilla Mux
go get -u github.com/slipros/roamer/pkg/gorilla@latest

# HttpRouter
go get -u github.com/slipros/roamer/pkg/httprouter@latest

Quick Start

package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/decoder"
	"github.com/slipros/roamer/formatter"
	"github.com/slipros/roamer/parser"
)

// Define request struct with tags
type CreateUserRequest struct {
	Name      string `json:"name" string:"trim_space"`
	Email     string `json:"email" string:"trim_space,lower"`
	Age       int    `query:"age" numeric:"min=18"`
	UserAgent string `header:"User-Agent"`
}

func main() {
	// Initialize roamer
	r := roamer.NewRoamer(
		roamer.WithDecoders(decoder.NewJSON()),
		roamer.WithParsers(
			parser.NewHeader(),
			parser.NewQuery(),
		),
		roamer.WithFormatters(
			formatter.NewString(),
			formatter.NewNumeric(),
		),
	)

	// Create handler
	http.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) {
		var userReq CreateUserRequest

		// Parse request
		if err := r.Parse(req, &userReq); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		// Use parsed data
		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(map[string]any{
			"name":       userReq.Name,
			"email":      userReq.Email,
			"age":        userReq.Age,
			"user_agent": userReq.UserAgent,
		}); err != nil {
			log.Printf("Failed to encode response: %v", err)
			http.Error(w, "Failed to encode response", http.StatusInternalServerError)
			return
		}
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Examples

Comprehensive examples are available in the examples/ directory:

Basic Usage
Router Integration
Advanced Features

See the examples README for a complete list and how to run them.

Struct Tags Reference

Data Source Tags
Tag Description Example
json Parse from JSON body json:"name"
xml Parse from XML body xml:"name"
form Parse from URL-encoded form form:"name"
multipart Parse from multipart form multipart:"file"
query Parse from query parameters query:"page"
header Parse from HTTP headers header:"User-Agent"
cookie Parse from cookies cookie:"session_id"
path Parse from path variables path:"id"
default Default value if not present default:"1"
Formatter Tags
Tag Operations Example
string trim_space, lower, upper, title, snake, camel, kebab, slug string:"trim_space,lower"
numeric min=N, max=N, abs, round, ceil, floor numeric:"min=0,max=100"
time timezone=TZ, truncate=UNIT, start_of_day, end_of_day time:"timezone=UTC"
slice unique, sort, sort_desc, compact, limit=N slice:"unique,sort"

Creating Extensions

Custom Parser
type CustomParser struct{}

func (p *CustomParser) Parse(r *http.Request, tag reflect.StructTag, _ parser.Cache) (any, bool) {
	tagValue, ok := tag.Lookup("custom")
	if !ok {
		return "", false
	}
	// Extract and return value
	return value, true
}

func (p *CustomParser) Tag() string {
	return "custom"
}
Custom Decoder
type CustomDecoder struct{}

func (d *CustomDecoder) Decode(r *http.Request, ptr any) error {
	// Decode request body into ptr
	return nil
}

func (d *CustomDecoder) ContentType() string {
	return "application/custom"
}
Custom Formatter
type CustomFormatter struct{}

func (f *CustomFormatter) Format(tag reflect.StructTag, ptr any) error {
	tagValue, ok := tag.Lookup("custom_format")
	if !ok {
		return nil
	}
	// Format the value pointed to by ptr
	return nil
}

func (f *CustomFormatter) Tag() string {
	return "custom_format"
}

See the examples/ directory for complete custom extension examples.

Performance

Roamer is designed for production use with:

  • Efficient reflection techniques
  • Caching for improved performance
  • Optional sync.Pool support for high-throughput applications
  • Minimal allocations in hot paths

Best Practices

  1. Separate request and response structs - Use dedicated structs for parsing requests
  2. Endpoint-specific structs - Create tailored structs for each endpoint to minimize overhead
  3. Use formatters - Let roamer handle common transformations (trimming, case conversion, etc.)
  4. Combine with validation - Use roamer for parsing, then validate with libraries like validator.go

Documentation

Contributing

Contributions are welcome! Please submit issues or pull requests.

License

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

Documentation

Overview

Package roamer provides a flexible HTTP request parser for Go applications.

It extracts data from various parts of an HTTP request (headers, query parameters, cookies, path variables, request body) into Go structures using struct tags, simplifying REST API development and request validation.

Key Features

  • Declarative request parsing using struct tags
  • Support for multiple data sources (query, headers, cookies, path, body)
  • Pluggable parsers, decoders, and formatters
  • Built-in type conversion and validation
  • Thread-safe and reusable across requests
  • Memory-efficient with object pooling support

Basic Usage

Define a request structure with tags:

type UserRequest struct {
    ID        int    `query:"id"`           // From URL query parameters
    Name      string `json:"name"`          // From JSON request body
    UserAgent string `header:"User-Agent"`  // From HTTP headers
    Session   string `cookie:"session_id"`  // From cookies
}

Create a Roamer instance with required components:

r := roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithParsers(
        parser.NewQuery(),
        parser.NewHeader(),
        parser.NewCookie(),
    ),
)

Parse an HTTP request:

var user UserRequest
err := r.Parse(request, &user)
if err != nil {
    // Handle parsing error
}

Advanced Features

Value formatting after parsing:

type FormRequest struct {
    Email string `json:"email" string:"trim_space,lower"`
    Age   int    `query:"age" numeric:"min=0,max=120"`
}

Multiple content types:

r := roamer.NewRoamer(
    roamer.WithDecoders(
        decoder.NewJSON(),
        decoder.NewXML(),
        decoder.NewFormURL(),
        decoder.NewMultipartFormData(),
    ),
)

Generic parsing with type inference:

userData, err := roamer.Parse[UserRequest](r, request)

Middleware integration:

http.Handle("/api/users", roamer.Middleware[UserRequest](r)(handler))

For more examples and detailed documentation, see https://github.com/slipros/roamer

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ContextWithParsedData

func ContextWithParsedData(ctx context.Context, data any) context.Context

ContextWithParsedData creates a new context containing the parsed data.

This function is used internally by roamer middleware (Middleware, SliceMiddleware) to store parsed request data in the context for downstream handlers to retrieve using ParsedDataFromContext.

Parameters:

  • ctx: The parent context.
  • data: The parsed data to store (typically a pointer to a struct or slice).

Returns:

  • context.Context: A new context containing the parsed data.

func ContextWithParsingError added in v1.8.0

func ContextWithParsingError(ctx context.Context, err error) context.Context

ContextWithParsingError creates a new context containing a parsing error.

This function is used internally by roamer middleware to propagate parsing errors to downstream handlers. The error can be retrieved using ParsedDataFromContext, which will return it instead of the parsed data.

Parameters:

  • ctx: The parent context.
  • err: The parsing error that occurred.

Returns:

  • context.Context: A new context containing the error.

func IsDecodeError

func IsDecodeError(err error) (rerr.DecodeError, bool)

IsDecodeError checks if an error is a DecodeError from request body parsing. Useful for providing specific error handling for body decoding failures.

Example:

if err := roamer.Parse(req, &data); err != nil {
    if decodeErr, ok := roamer.IsDecodeError(err); ok {
        // Special handling for decode errors
        http.Error(w, "Invalid request format", http.StatusBadRequest)
        return
    }
    // Handle other errors
}
Example

ExampleIsDecodeError demonstrates how to detect and handle decode errors.

package main

import (
	"bytes"
	"fmt"
	"net/http"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/decoder"
)

// readCloser wraps a bytes.Buffer to implement io.ReadCloser
type readCloser struct {
	*bytes.Buffer
}

func (rc *readCloser) Close() error {
	return nil
}

func main() {
	// Define a structure
	type User struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}

	// Create roamer with JSON decoder
	r := roamer.NewRoamer(
		roamer.WithDecoders(decoder.NewJSON()),
	)

	// Create request with invalid JSON body (malformed JSON)
	invalidJSON := `{invalid}` // Malformed JSON to cause parsing error
	req := &http.Request{
		Method: "POST",
		Header: http.Header{
			"Content-Type": {"application/json"},
		},
		Body:          &readCloser{bytes.NewBufferString(invalidJSON)},
		ContentLength: int64(len(invalidJSON)),
	}

	var user User
	err := r.Parse(req, &user)
	if err != nil {
		// Check if it's a decode error
		if decodeErr, isDecodeError := roamer.IsDecodeError(err); isDecodeError {
			fmt.Printf("Decode error occurred: %v\n", decodeErr)
			// Handle decode error specifically
			return
		}

		// Handle other types of errors
		fmt.Printf("Other error: %v\n", err)
		return
	}

	fmt.Printf("User parsed successfully: %+v\n", user)

}
Output:

Decode error occurred: decode `application/json` request body for `*roamer_test.User`: roamer_test.User.readFieldHash: expect ", but found i, error found in #2 byte of ...|{invalid}|..., bigger context ...|{invalid}|...

func IsSliceIterationError

func IsSliceIterationError(err error) (rerr.SliceIterationError, bool)

IsSliceIterationError checks if an error occurred during slice iteration. Provides access to the specific index where the error occurred.

Example:

if err := processItems(items); err != nil {
    if iterErr, ok := roamer.IsSliceIterationError(err); ok {
        // Access problem index with iterErr.Index
        return fmt.Errorf("item %d invalid: %w", iterErr.Index, iterErr.Err)
    }
}
Example

ExampleIsSliceIterationError demonstrates detecting slice iteration errors.

package main

import (
	"fmt"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/err"
)

func main() {
	// This example simulates a scenario where slice iteration might fail
	// In practice, this would occur during complex slice processing operations

	// Create a mock slice iteration error for demonstration
	originalErr := fmt.Errorf("validation failed")
	sliceErr := err.SliceIterationError{
		Err:   originalErr,
		Index: 2,
	}

	// Simulate checking the error
	var testErr error = sliceErr

	if iterErr, isSliceError := roamer.IsSliceIterationError(testErr); isSliceError {
		fmt.Printf("Slice iteration error at index %d: %v\n", iterErr.Index, iterErr.Err)
	} else {
		fmt.Printf("Not a slice iteration error: %v\n", testErr)
	}

}
Output:

Slice iteration error at index 2: validation failed

func Middleware

func Middleware[T any](roamer *Roamer) func(next http.Handler) http.Handler

Middleware creates an HTTP middleware that parses requests into a specified type and stores the result in the request context for downstream handlers to retrieve.

The middleware parses the incoming HTTP request using the provided Roamer instance, stores the parsed data (or error) in the request context, and then calls the next handler in the chain. Downstream handlers can retrieve the parsed data using ParsedDataFromContext.

Type Parameter

  • T: The type to parse the request into. Must be a struct type.

Behavior

  • If roamer is nil, the middleware passes through without parsing
  • On successful parsing, stores parsed data in context with ContextKeyParsedData
  • On parsing error, stores error in context with ContextKeyParsingError
  • Always calls the next handler, even if parsing fails (error handling is delegated)

Error Handling

The middleware does NOT stop the request chain on parsing errors. Instead, it stores the error in the context and delegates error handling to downstream handlers. This allows handlers to decide how to respond to parsing errors.

Parameters:

  • roamer: The Roamer instance to use for parsing. If nil, middleware is a no-op.

Returns:

  • func(next http.Handler) http.Handler: A middleware function that wraps the next handler.

Example:

type UserRequest struct {
    ID   int    `query:"id"`
    Name string `json:"name"`
}

r := roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithParsers(parser.NewQuery()),
)

// Use with http.Handle
http.Handle("/users", roamer.Middleware[UserRequest](r)(
    http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var data UserRequest
        if err := roamer.ParsedDataFromContext(r.Context(), &data); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        fmt.Fprintf(w, "Hello, %s (ID: %d)!", data.Name, data.ID)
    }),
))

// Or with chi router
router := chi.NewRouter()
router.Use(roamer.Middleware[UserRequest](r))
router.Post("/users", func(w http.ResponseWriter, r *http.Request) {
    var data UserRequest
    if err := roamer.ParsedDataFromContext(r.Context(), &data); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // Process user...
})
Example

ExampleMiddleware demonstrates using roamer as HTTP middleware.

package main

import (
	"fmt"
	"net/http"
	"net/url"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/parser"
)

func main() {
	type APIRequest struct {
		Action string `query:"action"`
		UserID int    `query:"user_id"`
	}

	// Create roamer instance
	r := roamer.NewRoamer(
		roamer.WithParsers(parser.NewQuery()),
	)

	// Create middleware
	middleware := roamer.Middleware[APIRequest](r)

	// Sample handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var data APIRequest
		if err := roamer.ParsedDataFromContext(r.Context(), &data); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		fmt.Printf("Action: %s, User ID: %d\n", data.Action, data.UserID)
		w.WriteHeader(http.StatusOK)
	})

	// Wrap handler with middleware
	wrappedHandler := middleware(handler)

	// Create test request
	req := &http.Request{
		Method: "GET",
		URL: &url.URL{
			RawQuery: "action=update&user_id=456",
		},
		Header: make(http.Header),
	}

	// Simulate request handling
	wrappedHandler.ServeHTTP(&mockResponseWriter{}, req)

}

type mockResponseWriter struct {
	statusCode int
}

func (m *mockResponseWriter) Header() http.Header {
	return make(http.Header)
}

func (m *mockResponseWriter) Write([]byte) (int, error) {
	return 0, nil
}

func (m *mockResponseWriter) WriteHeader(statusCode int) {
	m.statusCode = statusCode
}
Output:

Action: update, User ID: 456

func NewParseWithPool added in v1.16.0

func NewParseWithPool[T any](r *Roamer) func(req *http.Request, callback func(*T) error) error

NewParseWithPool creates a memory-efficient parser function that uses object pooling to reduce allocations when processing HTTP requests. This is particularly useful for high-throughput scenarios where creating new instances repeatedly would create unnecessary garbage collection pressure.

The returned function parses HTTP requests into instances of type T from a sync.Pool, passes the parsed instance to the callback function, and then returns it to the pool after zeroing its fields. This ensures memory reuse while preventing data leakage between requests.

Performance characteristics:

  • Reduces heap allocations by reusing instances of type T
  • Automatically zeros out fields before returning to pool
  • Thread-safe for concurrent request processing
  • Minimal overhead compared to direct parsing

Example:

type UserRequest struct {
    ID   int    `query:"id"`
    Name string `json:"name"`
}

r := roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithParsers(parser.NewQuery()),
)

// Create a pooled parser function
parseUser := roamer.NewParseWithPool[UserRequest](r)

// Use in HTTP handler
http.HandleFunc("/user", func(w http.ResponseWriter, req *http.Request) {
    err := parseUser(req, func(user *UserRequest) error {
        // Process the parsed user data
        // The user instance will be automatically returned to pool
        return processUser(user)
    })
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
})

Parameters:

  • r: The configured Roamer instance to use for parsing requests.

Returns:

  • A function that accepts an HTTP request and a callback function. The callback receives a pointer to the parsed data of type T. Returns an error if parsing fails or if the callback returns an error.

Notes:

  • The callback function must not retain references to the parsed instance after returning, as it will be zeroed and returned to the pool.
  • If you need to keep the data, copy it to a new instance within the callback.
  • The pool grows dynamically based on concurrent usage patterns.

func Parse added in v1.14.0

func Parse[T any](r *Roamer, req *http.Request) (T, error)

Parse is a generic function that extracts data from an HTTP request into a value of type T. This is a convenience wrapper around the Roamer.Parse method that returns the parsed value directly instead of requiring a pointer parameter.

The function creates a zero value of type T, parses the request data into it, and returns both the result and any error that occurred during parsing.

Example:

type UserData struct {
    ID        int    `query:"id"`
    Name      string `json:"name"`
    UserAgent string `header:"User-Agent"`
}

// Parse request data directly into a value
userData, err := roamer.Parse[UserData](roamer, request)
if err != nil {
    return err
}
// Use userData...

Parameters:

  • r: The configured Roamer instance to use for parsing.
  • req: The HTTP request to parse data from.

Returns:

  • T: The parsed data structure of the specified type.
  • error: An error if parsing fails, or nil if successful.
Example

ExampleParse demonstrates the generic Parse function for direct value retrieval.

package main

import (
	"fmt"
	"net/http"
	"net/url"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/parser"
)

func main() {
	// Define a structure
	type ProductRequest struct {
		Category string  `query:"category"`
		MinPrice float64 `query:"min_price"`
		MaxPrice float64 `query:"max_price"`
	}

	// Create roamer instance
	r := roamer.NewRoamer(
		roamer.WithParsers(parser.NewQuery()),
	)

	// Create request with query parameters
	req := &http.Request{
		Method: "GET",
		URL: &url.URL{
			RawQuery: "category=electronics&min_price=100.50&max_price=999.99",
		},
		Header: make(http.Header),
	}

	// Use generic Parse function
	product, err := roamer.Parse[ProductRequest](r, req)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Category: %s\n", product.Category)
	fmt.Printf("Price range: $%.2f - $%.2f\n", product.MinPrice, product.MaxPrice)

}
Output:

Category: electronics
Price range: $100.50 - $999.99

func ParsedDataFromContext

func ParsedDataFromContext[T any](ctx context.Context, ptr *T) error

ParsedDataFromContext extracts parsed request data from a context into the provided pointer.

This function is typically used in HTTP handlers to retrieve data that was previously parsed and stored in the context by roamer middleware (see Middleware or SliceMiddleware).

Error Handling

The function returns an error if:

  • The pointer is nil (NilValue error)
  • A parsing error occurred and is stored in the context
  • No data of the expected type is found in the context (NoData error)

Type Safety

The function uses generics to ensure type safety. The type parameter T must match the type used when storing data in the context, otherwise a NoData error is returned.

Parameters:

  • ctx: The context containing the parsed data (typically from http.Request.Context()).
  • ptr: A pointer to the destination variable where the data will be copied.

Returns:

  • error: An error if retrieval fails, or nil if successful.

Example:

type UserRequest struct {
    ID   int    `query:"id"`
    Name string `json:"name"`
}

func MyHandler(w http.ResponseWriter, r *http.Request) {
    var userData UserRequest
    if err := roamer.ParsedDataFromContext(r.Context(), &userData); err != nil {
        if errors.Is(err, err.NoData) {
            http.Error(w, "No user data in context", http.StatusInternalServerError)
            return
        }
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // Use userData...
    fmt.Fprintf(w, "Hello, %s!", userData.Name)
}

func SliceMiddleware added in v1.9.0

func SliceMiddleware[T any](roamer *Roamer) func(next http.Handler) http.Handler

SliceMiddleware creates an HTTP middleware that parses the request body into a slice of the specified type and stores the result in the request context.

This middleware is particularly useful for API endpoints that handle arrays of objects, such as batch operations, bulk updates, or list submissions. It parses the request body (typically JSON) into a slice and makes it available to downstream handlers via the context.

Type Parameter

  • T: The element type of the slice. The request body will be parsed into []T.

Behavior

  • If roamer is nil, the middleware passes through without parsing
  • Parses the request body into a slice of type []T
  • On success, stores the slice in context with ContextKeyParsedData
  • On error, stores the error in context with ContextKeyParsingError
  • Always calls the next handler (error handling is delegated)

Use Cases

  • Batch creation endpoints: POST /api/users/batch with [{...}, {...}, ...]
  • Bulk update operations: PUT /api/products/bulk with array of products
  • List submission forms: POST /api/tasks with array of task items

Parameters:

  • roamer: The Roamer instance to use for parsing. If nil, middleware is a no-op.

Returns:

  • func(next http.Handler) http.Handler: A middleware function that wraps the next handler.

Example:

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

r := roamer.NewRoamer(roamer.WithDecoders(decoder.NewJSON()))

// Batch product creation endpoint
http.Handle("/products/batch", roamer.SliceMiddleware[Product](r)(
    http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var products []Product
        if err := roamer.ParsedDataFromContext(r.Context(), &products); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        // Validate and process the batch
        if len(products) == 0 {
            http.Error(w, "Empty product list", http.StatusBadRequest)
            return
        }

        for i, product := range products {
            // Process each product
            fmt.Printf("Processing product %d: %s\n", i, product.Name)
        }

        fmt.Fprintf(w, "Successfully processed %d products", len(products))
    }),
))

// Request body example:
// [
//   {"id": 1, "name": "Widget", "price": 9.99},
//   {"id": 2, "name": "Gadget", "price": 19.99}
// ]

Types

type AfterParser

type AfterParser interface {
	// AfterParse is called after the HTTP request has been successfully parsed.
	// This method can be used to perform additional validation, data transformation,
	// or business logic based on the parsed data.
	AfterParse(r *http.Request) error
}

AfterParser is an interface that can be implemented by the target struct to execute custom logic after the HTTP request has been parsed.

type AssignExtensions added in v1.15.0

type AssignExtensions interface {
	// AssignExtensions returns a slice of extension functions that provide
	// custom value assignment capabilities for specific types.
	//
	// Each ExtensionFunc takes a value and returns:
	//   - An assignment function that handles the conversion, if the extension
	//     can process the given value type
	//   - A boolean indicating whether this extension can handle the value
	//
	// Returns:
	//   A slice of extension functions for custom type assignments.
	AssignExtensions() []assign.ExtensionFunc
}

AssignExtensions is an optional interface that can be implemented by parsers and decoders to provide custom value assignment extensions. These extensions allow for specialized handling of complex types during the assignment process.

When a parser or decoder implements this interface, its extension functions are automatically registered with the assignment system during Roamer initialization. This enables custom type conversions and value transformations beyond the standard assignment capabilities.

Example implementation:

func (p *CustomParser) AssignExtensions() []assign.ExtensionFunc {
    return []assign.ExtensionFunc{
        func(value any) (func(to reflect.Value) error, bool) {
            // Custom assignment logic for specific types
            if customType, ok := value.(MyCustomType); ok {
                return func(to reflect.Value) error {
                    // Convert and assign the custom type
                    return assign.String(to, customType.String())
                }, true
            }
            return nil, false
        },
    }
}

type ContextKey

type ContextKey uint8

ContextKey represents a type-safe key for context values used by the roamer package. Using a custom type for context keys helps prevent collisions with other packages.

const (
	// ContextKeyParsedData is the context key for storing parsed data from HTTP requests.
	// This key is used to retrieve the parsed data in handlers after middleware processing.
	ContextKeyParsedData ContextKey = iota + 1

	// ContextKeyParsingError is the context key for storing any parsing errors
	// that occurred during request processing. This allows propagating parsing
	// errors to downstream handlers.
	ContextKeyParsingError
)

type Decoder

type Decoder interface {
	// Decode parses the body of an HTTP request into the provided pointer.
	// The implementation should determine how to handle the request body
	// based on the Content-Type header.
	//
	// Parameters:
	//   - r: The HTTP request containing the body to decode.
	//   - ptr: A pointer to the target value where the decoded data will be stored.
	//
	// Returns:
	//   - error: An error if decoding fails, or nil if successful.
	Decode(r *http.Request, ptr any) error

	// ContentType returns the Content-Type header value that this decoder handles.
	// For example, a JSON decoder might return "application/json",
	// an XML decoder might return "application/xml", etc.
	ContentType() string

	// Tag returns the struct tag name used for field mapping.
	// For example, a JSON decoder returns "json", XML decoder returns "xml", etc.
	Tag() string
}

Decoder is an interface for components that decode HTTP request bodies based on the Content-Type header. Different decoders can handle different formats (JSON, XML, form data, etc.).

Implementing a custom decoder allows extending the functionality of the roamer package to support additional content types or custom parsing logic.

type Decoders

type Decoders map[string]Decoder

Decoders is a map of registered decoders where keys are the Content-Type header values returned by the Decoder.ContentType() method.

func (Decoders) Tags added in v1.12.1

func (ds Decoders) Tags() []string

Tags returns all struct tag names from the registered decoders. This is useful for determining which struct tags are supported by the current decoder set.

type Formatter added in v1.8.0

type Formatter interface {
	// Format transforms a field value based on the provided struct tag.
	// The implementation should check if the tag contains relevant formatting
	// instructions and apply them to the value referenced by the pointer.
	//
	// Parameters:
	//   - tag: The struct tag containing formatting instructions.
	//   - ptr: A pointer to the value to be formatted.
	//
	// Returns:
	//   - error: An error if formatting fails, or nil if successful.
	Format(tag reflect.StructTag, ptr any) error

	// Tag returns the name of the struct tag that this formatter handles.
	// For example, a string formatter might return "string",
	// a number formatter might return "number", etc.
	Tag() string
}

Formatter is an interface for components that post-process parsed field values based on struct tags. Formatters can be used to transform values after they have been parsed from the request (e.g., trimming strings, converting case, etc.).

Implementing a custom formatter allows extending the functionality of the roamer package to support additional transformations on parsed values.

type Formatters added in v1.8.0

type Formatters map[string]Formatter

Formatters is a map of registered formatters where keys are the tag names returned by the Formatter.Tag() method.

type OptionsFunc

type OptionsFunc func(*Roamer)

OptionsFunc is a function type for configuring a Roamer instance. It follows the functional options pattern to provide a clean and extensible API for customizing the behavior of the parser.

func WithAssignExtensions added in v1.15.0

func WithAssignExtensions(extensions ...assign.ExtensionFunc) OptionsFunc

WithAssignExtensions registers additional assignment extension functions. These extensions provide custom value assignment capabilities for specific types that require special handling beyond standard type conversions.

Assignment extensions are functions that take a value and return an assignment function if they can handle that value type. This allows for sophisticated type handling and custom conversion logic.

Example:

customExtension := func(value any) (func(to reflect.Value) error, bool) {
    if customType, ok := value.(MyCustomType); ok {
        return func(to reflect.Value) error {
            // Custom assignment logic
            return assign.String(to, customType.String())
        }, true
    }
    return nil, false
}

r := roamer.NewRoamer(
    roamer.WithAssignExtensions(customExtension),
)

Note: Extensions from parsers and decoders that implement AssignExtensions interface are automatically registered. Use this function for standalone extension functions that are not tied to specific parsers or decoders.

func WithDecoders

func WithDecoders(decoders ...Decoder) OptionsFunc

WithDecoders registers decoders for parsing request bodies. Decoders handle different content types like JSON, XML, or form data.

Example:

r := roamer.NewRoamer(
    roamer.WithDecoders(
        decoder.NewJSON(),               // JSON bodies
        decoder.NewFormURL(),            // URL-encoded forms
        decoder.NewMultipartFormData(),  // Multipart forms
    ),
)

func WithFormatters added in v1.8.0

func WithFormatters(formatters ...Formatter) OptionsFunc

WithFormatters registers formatters that process values after parsing. Formatters handle operations like string trimming or case conversion.

Example:

r := roamer.NewRoamer(
    roamer.WithFormatters(
        formatter.NewString(), // Apply 'string' tag formatters
    ),
)

// Example usage:
type User struct {
    Name string `json:"name" string:"trim_space"` // Trim spaces
}

func WithParsers

func WithParsers(parsers ...Parser) OptionsFunc

WithParsers registers parsers that extract data from HTTP requests. Parsers handle different parts of a request based on struct tags.

Example:

r := roamer.NewRoamer(
    roamer.WithParsers(
        parser.NewQuery(),    // 'query' tag for URL parameters
        parser.NewHeader(),   // 'header' tag for HTTP headers
        parser.NewCookie(),   // 'cookie' tag for cookies
    ),
)

func WithPreserveBody added in v1.16.0

func WithPreserveBody() OptionsFunc

WithPreserveBody enables preservation of the request body after decoding. The decoder reads the entire body into memory, decodes it, and then replaces http.Request.Body with a new io.ReadCloser containing the same data. This allows downstream handlers to read the body again.

WARNING: This option increases memory usage as the entire request body is buffered in memory. Use with caution for large request bodies. Consider implementing size limits in your HTTP server to prevent excessive memory consumption.

Example:

// Enable body preservation for middleware that needs to read body multiple times
r := roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithPreserveBody(),
)

// Now downstream handlers can also read the body
func handler(w http.ResponseWriter, r *http.Request) {
    // Body was already read by roamer for parsing,
    // but can be read again here thanks to preservation
    body, _ := io.ReadAll(r.Body)
    // ... use body ...
}
Example

ExampleWithPreserveBody demonstrates how to preserve the request body after parsing so downstream handlers can read it again.

type RequestData struct {
	Message string `json:"message"`
	UserID  int    `json:"user_id"`
}

// Create roamer with body preservation enabled
r := roamer.NewRoamer(
	roamer.WithDecoders(decoder.NewJSON()),
	roamer.WithPreserveBody(), // Enable body preservation
)

// Create a sample request with JSON body
jsonBody := `{"message": "Hello, World!", "user_id": 123}`
req := &http.Request{
	Method: "POST",
	URL: &url.URL{
		Path: "/api/data",
	},
	Header: http.Header{
		"Content-Type": {"application/json"},
	},
	Body:          &readCloser{bytes.NewBufferString(jsonBody)},
	ContentLength: int64(len(jsonBody)),
}

// Parse the request (this normally consumes the body)
var data RequestData
err := r.Parse(req, &data)
if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}

fmt.Printf("Parsed: Message=%s, UserID=%d\n", data.Message, data.UserID)

// Read the body again - this works because preservation is enabled
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
	fmt.Printf("Error reading body: %v\n", err)
	return
}

fmt.Printf("Body still available: %s\n", string(bodyBytes))
Output:

Parsed: Message=Hello, World!, UserID=123
Body still available: {"message": "Hello, World!", "user_id": 123}

func WithSkipFilled

func WithSkipFilled(skip bool) OptionsFunc

WithSkipFilled controls whether to skip fields with non-zero values. When true (default), existing non-zero values won't be overwritten.

Example:

// Override even filled fields
r := roamer.NewRoamer(
    roamer.WithSkipFilled(false),
)

type Parser

type Parser interface {
	// Parse extracts data from an HTTP request based on the provided struct tag.
	// It returns the parsed value and a boolean indicating whether parsing was successful.
	// The cache parameter can be used to store intermediate results for performance optimization.
	//
	// Parameters:
	//   - r: The HTTP request to parse data from.
	//   - tag: The struct tag containing parsing instructions.
	//   - cache: A cache for storing intermediate parsing results.
	//
	// Returns:
	//   - any: The parsed value (can be of any type).
	//   - bool: Whether parsing was successful (false if no matching data was found).
	Parse(r *http.Request, tag reflect.StructTag, cache parser.Cache) (any, bool)

	// Tag returns the name of the struct tag that this parser handles.
	// For example, a query parameter parser might return "query",
	// a header parser might return "header", etc.
	Tag() string
}

Parser is an interface for components that extract data from specific parts of an HTTP request based on struct tags. Different parsers can handle different parts of the request (headers, query parameters, cookies, path parameters, etc.).

Implementing a custom parser allows extending the functionality of the roamer package to support additional data sources or custom parsing logic.

type Parsers

type Parsers map[string]Parser

Parsers is a map of registered parsers where keys are the tag names returned by the Parser.Tag() method.

type ReflectValueFormatter added in v1.13.0

type ReflectValueFormatter interface {
	// FormatReflectValue transforms a field value directly using a reflect.Value.
	// This method is called instead of Format when a formatter implements this interface,
	// allowing for more efficient operations by avoiding any conversions.
	//
	// Parameters:
	//   - tag: The struct tag containing formatting instructions.
	//   - val: The reflect.Value to be formatted directly.
	//
	// Returns:
	//   - error: An error if formatting fails, or nil if successful.
	FormatReflectValue(tag reflect.StructTag, val reflect.Value) error
}

ReflectValueFormatter is an optional interface that can be implemented by a Formatter. If a Formatter implements this interface, the FormatReflectValue method will be called instead of Format. This allows the Formatter to work directly with a reflect.Value, which can be more efficient in some cases.

type ReflectValueFormatters added in v1.14.0

type ReflectValueFormatters map[string]ReflectValueFormatter

ReflectValueFormatters is a map of registered reflect value formatters where keys are the tag names returned by the Formatter.Tag() method.

type RequireStructureCache added in v1.12.0

type RequireStructureCache interface {
	// SetStructureCache provides the component with a structure cache instance.
	// This method is called once during Roamer initialization to pass
	// the cache to components that need it for performance optimization.
	//
	// Parameters:
	//   - cache: The structure cache instance for storing field metadata.
	SetStructureCache(cache *cache.Structure)
}

RequireStructureCache is an interface for components that require a structure cache for efficient field analysis and caching.

Components implementing this interface will receive a structure cache instance during Roamer initialization, allowing them to optimize reflection operations by caching struct field metadata.

This interface is typically implemented by decoders that need to perform repetitive struct field analysis for the same types.

type Roamer

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

Roamer is a flexible HTTP request parser that extracts data from various parts of an HTTP request into Go structures using struct tags.

It coordinates the work of parsers (for headers, query params, cookies, path), decoders (for request body), and formatters (for post-processing values).

Thread Safety

A Roamer instance is safe for concurrent use and should be reused across multiple HTTP requests. Creating a Roamer is relatively expensive due to reflection-based setup, so instances should be created once (typically during application initialization) and reused.

Performance Considerations

  • Roamer uses internal caching to optimize struct field analysis
  • Parser cache pools reduce allocations during request processing
  • Structure metadata is cached per type to avoid repeated reflection

Memory Management

For high-throughput scenarios, consider using NewParseWithPool to further reduce allocations by reusing request struct instances.

Example:

// Create once at application startup
var globalRoamer = roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithParsers(parser.NewQuery(), parser.NewHeader()),
    roamer.WithFormatters(formatter.NewString()),
)

// Reuse across all handlers
func handler(w http.ResponseWriter, r *http.Request) {
    var req MyRequest
    err := globalRoamer.Parse(r, &req)
    // ...
}
Example

ExampleRoamer demonstrates basic usage of the roamer package for parsing HTTP requests into Go structures.

// Define a structure to hold parsed request data
type UserRequest struct {
	ID        int    `query:"id"`
	Name      string `json:"name"`
	UserAgent string `header:"User-Agent"`
	Email     string `json:"email" string:"trim_space"`
}

// Create a roamer instance with parsers, decoders, and formatters
r := roamer.NewRoamer(
	roamer.WithParsers(
		parser.NewQuery(),  // Parse URL query parameters
		parser.NewHeader(), // Parse HTTP headers
	),
	roamer.WithDecoders(
		decoder.NewJSON(), // Decode JSON request bodies
	),
	roamer.WithFormatters(
		formatter.NewString(), // Apply string formatting
	),
)

// Create a sample HTTP request
req := createSampleRequest()

// Parse the request
var userData UserRequest
err := r.Parse(req, &userData)
if err != nil {
	fmt.Printf("Error parsing request: %v\n", err)
	return
}

fmt.Printf("Parsed data:\n")
fmt.Printf("ID: %d\n", userData.ID)
fmt.Printf("Name: %s\n", userData.Name)
fmt.Printf("Email: %s\n", userData.Email)
fmt.Printf("User-Agent: %s\n", userData.UserAgent)
Output:

Parsed data:
ID: 123
Name: John Doe
Email: [email protected]
User-Agent: test-agent

func NewRoamer

func NewRoamer(opts ...OptionsFunc) *Roamer

NewRoamer creates a configured Roamer instance with optional configuration.

The function accepts variadic OptionsFunc parameters that configure parsers, decoders, formatters, and other behavioral settings. The returned Roamer is safe for concurrent use and should be reused across multiple requests for optimal performance.

Configuration Options

  • WithDecoders: Register decoders for handling different content types
  • WithParsers: Register parsers for extracting data from request parts
  • WithFormatters: Register formatters for post-processing parsed values
  • WithSkipFilled: Control whether to overwrite non-zero field values
  • WithAssignExtensions: Add custom type conversion logic
  • WithPreserveBody: Enable request body preservation for downstream handlers

Default Behavior

By default, NewRoamer creates an instance that:

  • Skips already filled fields (skipFilled = true)
  • Has no parsers, decoders, or formatters (must be configured)
  • Uses internal caching for performance optimization

Parameters:

  • opts: Optional configuration functions to customize the Roamer instance.

Returns:

  • *Roamer: A configured Roamer instance ready for use.

Example:

// Basic Roamer with JSON decoder and query parser
r := roamer.NewRoamer(
    roamer.WithDecoders(decoder.NewJSON()),
    roamer.WithParsers(parser.NewQuery()),
)

// Roamer with multiple components and custom settings
r := roamer.NewRoamer(
    roamer.WithDecoders(
        decoder.NewJSON(),
        decoder.NewFormURL(),
        decoder.NewMultipartFormData(),
    ),
    roamer.WithParsers(
        parser.NewQuery(),
        parser.NewHeader(),
        parser.NewCookie(),
    ),
    roamer.WithFormatters(
        formatter.NewString(),
        formatter.NewNumeric(),
    ),
    roamer.WithSkipFilled(false),  // Overwrite even filled fields
    roamer.WithPreserveBody(),     // Allow body re-reading
)

func (*Roamer) Parse

func (r *Roamer) Parse(req *http.Request, ptr any) error

Parse extracts data from an HTTP request into the provided pointer.

The method supports parsing into structs, slices, arrays, and maps:

  • Structs: Processes request body AND other parts (headers, query, cookies, path)
  • Slices/Arrays/Maps: Processes only the request body content

Parsing Process

For struct targets, Parse performs the following steps:

  1. Decodes request body based on Content-Type (if decoder is registered)
  2. Extracts values from query parameters, headers, cookies, path (if parsers are registered)
  3. Applies formatters to post-process field values (if formatters are registered)
  4. Calls AfterParse() if the target implements the AfterParser interface

Field Processing Rules

  • Fields are processed based on their struct tags (query, header, cookie, path, json, etc.)
  • By default, already filled (non-zero) fields are skipped (controlled by WithSkipFilled)
  • Default values can be specified using the "default" tag
  • Multiple formatters can be chained using comma-separated values

Error Handling

Parse returns an error if:

  • req is nil (NilValue error)
  • ptr is nil (NilValue error)
  • ptr is not a pointer (NotPtr error)
  • The underlying type is not supported (NotSupported error)
  • Body decoding fails (DecodeError - check with IsDecodeError)
  • Field assignment fails (type conversion error)
  • Formatter application fails
  • AfterParse() returns an error

Parameters:

  • req: The HTTP request to parse data from. Must not be nil.
  • ptr: A pointer to the destination (struct, slice, array, or map). Must not be nil.

Returns:

  • error: An error if parsing fails, or nil if successful.

Example:

type UserRequest struct {
    ID        int    `query:"id"`
    Name      string `json:"name"`
    UserAgent string `header:"User-Agent"`
    Email     string `json:"email" string:"trim_space,lower"`
}

var userData UserRequest
err := r.Parse(request, &userData)
if err != nil {
    if decodeErr, ok := roamer.IsDecodeError(err); ok {
        // Handle body decoding error
        return fmt.Errorf("invalid request body: %w", decodeErr)
    }
    return fmt.Errorf("request parsing failed: %w", err)
}

AfterParser Interface

Implement AfterParser to add custom validation or processing:

func (u *UserRequest) AfterParse(r *http.Request) error {
    if u.ID <= 0 {
        return errors.New("invalid user ID")
    }
    return nil
}

Directories

Path Synopsis
Package decoder provides components for decoding HTTP request bodies of various content types.
Package decoder provides components for decoding HTTP request bodies of various content types.
Package err contains error definitions for the roamer package.
Package err contains error definitions for the roamer package.
Package formatter provides value formatters for post-processing parsed data.
Package formatter provides value formatters for post-processing parsed data.
internal
Package parser provides components for extracting data from different parts of HTTP requests.
Package parser provides components for extracting data from different parts of HTTP requests.
pkg
chi module
gorilla module
httprouter module

Jump to

Keyboard shortcuts

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