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 ¶
- func ContextWithParsedData(ctx context.Context, data any) context.Context
- func ContextWithParsingError(ctx context.Context, err error) context.Context
- func IsDecodeError(err error) (rerr.DecodeError, bool)
- func IsSliceIterationError(err error) (rerr.SliceIterationError, bool)
- func Middleware[T any](roamer *Roamer) func(next http.Handler) http.Handler
- func NewParseWithPool[T any](r *Roamer) func(req *http.Request, callback func(*T) error) error
- func Parse[T any](r *Roamer, req *http.Request) (T, error)
- func ParsedDataFromContext[T any](ctx context.Context, ptr *T) error
- func SliceMiddleware[T any](roamer *Roamer) func(next http.Handler) http.Handler
- type AfterParser
- type AssignExtensions
- type ContextKey
- type Decoder
- type Decoders
- type Formatter
- type Formatters
- type OptionsFunc
- func WithAssignExtensions(extensions ...assign.ExtensionFunc) OptionsFunc
- func WithDecoders(decoders ...Decoder) OptionsFunc
- func WithFormatters(formatters ...Formatter) OptionsFunc
- func WithParsers(parsers ...Parser) OptionsFunc
- func WithPreserveBody() OptionsFunc
- func WithSkipFilled(skip bool) OptionsFunc
- type Parser
- type Parsers
- type ReflectValueFormatter
- type ReflectValueFormatters
- type RequireStructureCache
- type Roamer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContextWithParsedData ¶
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
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 ¶
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
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
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 ¶
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
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 ¶
Decoders is a map of registered decoders where keys are the Content-Type header values returned by the Decoder.ContentType() method.
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
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 ¶
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 ¶
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:
- Decodes request body based on Content-Type (if decoder is registered)
- Extracts values from query parameters, headers, cookies, path (if parsers are registered)
- Applies formatters to post-process field values (if formatters are registered)
- 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
}
Source Files
¶
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
|