Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Proposal: Universal Request Binding with ctx.Bind().All() #3363

Open
coderabbitai bot opened this issue Mar 19, 2025 · 0 comments
Open

Feature Proposal: Universal Request Binding with ctx.Bind().All() #3363

coderabbitai bot opened this issue Mar 19, 2025 · 0 comments

Comments

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2025

Universal Request Binding: ctx.Bind().All()

Overview

This proposal suggests adding a new All() binding method to the Fiber framework that would bind data from multiple request sources (query parameters, body, URL parameters, headers, cookies) into a single struct using a single method call.

Motivation

Currently, developers need to call multiple binding methods (c.BodyParser(), c.QueryParser(), etc.) to gather data from different sources. This leads to repetitive code and requires developers to manually handle conflicts when the same field might be present in multiple sources.

Proposed API

// All binds data from multiple sources into a single struct
func (b *Bind) All(out any) error

Usage Examples

Basic Usage

type User struct {
    // Field can be bound from any source
    ID        int    `param:"id" query:"id" json:"id" form:"id"`
    
    // Fields with source-specific tags
    Name      string `query:"name" json:"name" form:"name"`
    Email     string `json:"email" form:"email"`
    Role      string `header:"x-user-role"`
    SessionID string `cookie:"session_id"`
    
    // File upload support
    Avatar    *multipart.FileHeader `form:"avatar"`
}

app.Post("/users/:id", func(c fiber.Ctx) error {
    user := new(User)
    
    if err := c.Bind().All(user); err != nil {
        return err
    }
    
    // All available data is now bound to the user struct
    return c.JSON(user)
})

With Validation

type CreateProductRequest struct {
    Name        string  `json:"name" query:"name" validate:"required,min=3"`
    Price       float64 `json:"price" query:"price" validate:"required,gt=0"`
    CategoryID  int     `json:"category_id" param:"category_id" validate:"required"`
}

app.Post("/categories/:category_id/products", func(c fiber.Ctx) error {
    req := new(CreateProductRequest)
    
    if err := c.Bind().All(req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request data",
            "details": err.Error(),
        })
    }
    
    // Input is bound and validated
    return createProduct(c, req)
})

Source Precedence

When the same field is present in multiple sources, we need a clear precedence rule. Proposed default precedence order (highest to lowest):

  1. URL Parameters (:param)
  2. Body (JSON/XML/Form)
  3. Query parameters
  4. Headers
  5. Cookies

This order follows a general principle of specificity (URL param is most specific to this request), followed by explicit data (body), then ambient request data (query, headers, cookies).

Custom Source Precedence

For flexibility, developers can define custom precedence with an optional binding_source tag:

type Product struct {
    // Override default precedence (query takes precedence over body)
    ID    int    `param:"id" query:"id" json:"id" binding_source:"param,query,body"`
    
    // Only bind from form data, ignore other sources
    Image *multipart.FileHeader `form:"image" binding_source:"form"`
}

Empty Values Handling

By default, empty values from higher-precedence sources won't overwrite non-empty values from lower-precedence sources.

Example: If a field has value "foo" in the body but an empty string in the query, the final value would be "foo".

This behavior can be changed with a configuration option:

// Override empty values behavior
if err := c.Bind().WithOverrideEmptyValues(true).All(user); err != nil {
    return err
}

Implementation Details

Integration with Existing Bind Interface

The proposed feature would extend the existing Bind interface:

type Bind interface {
    // Existing methods
    Body(out any) error
    Query(out any) error
    Params(out any) error
    Headers(out any) error
    Cookies(out any) error
    
    // New methods
    All(out any) error
    WithOverrideEmptyValues(override bool) Bind
}

Internal Implementation Approach

  1. Parse each data source independently (reusing existing binding logic)
  2. Apply values to the output struct according to precedence rules
  3. Handle special cases like file uploads appropriately
  4. Provide detailed error messages for binding failures
// Pseudo-implementation
func (b *Bind) All(out any) error {
    // Store binding errors to report after all attempts
    var bindingErrors []error
    
    // Get values from each source
    paramValues, paramErr := b.getParamValues()
    if paramErr != nil {
        bindingErrors = append(bindingErrors, paramErr)
    }
    
    bodyValues, bodyErr := b.getBodyValues()
    if bodyErr != nil {
        bindingErrors = append(bindingErrors, bodyErr)
    }
    
    queryValues, queryErr := b.getQueryValues()
    if queryErr != nil {
        bindingErrors = append(bindingErrors, queryErr)
    }
    
    // ... similar for headers and cookies
    
    // Apply values according to precedence
    if err := b.applyValues(out, paramValues, bodyValues, queryValues, headerValues, cookieValues); err != nil {
        return err
    }
    
    // If we had any binding errors but still managed to bind some values, report them
    if len(bindingErrors) > 0 {
        return fmt.Errorf("partial binding completed with errors: %v", bindingErrors)
    }
    
    return nil
}

Performance Considerations

  1. Avoid unnecessary parsing (lazy-load each source)
  2. Reuse existing binding mechanisms where possible
  3. Minimize allocations for property mapping
  4. Consider caching struct field metadata for repeated bindings

Benefits

  1. Simplified API for handling data from multiple sources
  2. Reduced code duplication in handlers
  3. Clearer intent in struct definitions
  4. Consistent handling of conflicts between sources
  5. Better integration with validation libraries
  6. Aligns with RESTful practices where resources can be identified and manipulated through multiple means

Future Extensions

  1. Support for binding to nested structs from different sources
  2. Custom binding functions for specific fields
  3. Built-in type conversion for common cases (string to int, etc.)
  4. Integration with schema validation

This proposal aligns with Fiber's design principles of Express.js compatibility while offering Go-idiomatic features that improve developer experience.

References


Requested by: @ReneWerner87

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

1 participant