Binders

Overview

Binders are responsible for extracting and binding data from HTTP requests (query parameters, form data, JSON bodies) into structured input objects for Maestro operations. They provide a clean interface between raw HTTP data and typed Go structures.

Purpose

Binders serve several key functions: - Data Extraction - Parse HTTP requests and extract relevant data - Type Conversion - Convert string values to appropriate Go types - Input Preparation - Populate Maestro input structures with bound data - Request Context - Handle request-specific context and metadata

Structure

Each binder typically follows this pattern:

type NewUserAppBinder struct {
    Binder *binder.Binder
}

func (this *NewUserAppBinder) Bind(ctx context.Context, in *maes.NewUserMaeIn, c *gin.Context) error {
    this.bindRequest(ctx, in, c)
    return nil
}

Common Binding Methods

Query String Binding

func (this *SomeBinder) bindRequestForm(ctx context.Context, in *maes.SomeMaeIn, c *gin.Context) error {
    s := &in.Request.Form
    s.Name = this.Binder.QueryStringField("form[name]", c, &in.Context)
    s.Email = this.Binder.QueryStringField("form[email]", c, &in.Context)
    return nil
}

JSON Body Binding

func (this *SomeBinder) bindRequestJSON(ctx context.Context, in *maes.SomeMaeIn, c *gin.Context) error {
    var jsonData struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

    if err := c.ShouldBindJSON(&jsonData); err != nil {
        return err
    }

    in.Request.Form.Name = jsonData.Name
    in.Request.Form.Email = jsonData.Email
    return nil
}

Form Data Binding

func (this *SomeBinder) bindRequestForm(ctx context.Context, in *maes.SomeMaeIn, c *gin.Context) error {
    s := &in.Request.Form
    s.Name = c.PostForm("name")
    s.Email = c.PostForm("email")
    return nil
}

Integration with Maestros

Binders are typically called at the beginning of controller methods:

func (gc *GardenController) Create(c *gin.Context) {
    input := &maes.CreateGardenMaeIn{}

    // Bind request data
    if err := gc.CreateGardenBinder.Bind(c.Request.Context(), input, c); err != nil {
        c.JSON(400, gin.H{"error": "Invalid request data"})
        return
    }

    // Execute business logic
    output, err := gc.CreateGardenMae.Execute(c.Request.Context(), input)
    // ...
}

Request Context Handling

Binders also populate request context information:

func (this *SomeBinder) bindContext(ctx context.Context, in *maes.SomeMaeIn, c *gin.Context) error {
    // Extract user context
    if userID, exists := c.Get("user_id"); exists {
        in.Context.UserId = userID.(string)
    }

    // Set request metadata
    in.Context.RequestID = c.GetString("request_id")
    in.Context.Timestamp = time.Now()
    return nil
}

Error Handling

Binders should handle binding errors gracefully:

func (this *SomeBinder) Bind(ctx context.Context, in *maes.SomeMaeIn, c *gin.Context) error {
    if err := this.bindRequest(ctx, in, c); err != nil {
        return fmt.Errorf("failed to bind request: %w", err)
    }

    if err := this.validateBindings(in); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }

    return nil
}

Best Practices

Type Safety

  • Always convert string inputs to appropriate types
  • Handle conversion errors gracefully
  • Provide default values when appropriate

Validation

  • Perform basic format validation during binding
  • Leave business rule validation to validators
  • Return clear error messages for binding failures

Performance

  • Bind only necessary data
  • Avoid expensive operations in binders
  • Cache parsed values when appropriate

Security

  • Sanitize input data
  • Validate parameter names and values
  • Prevent parameter pollution attacks

Testing Binders

func TestCreateGardenBinder(t *testing.T) {
    binder := setupCreateGardenBinder()

    // Setup mock request
    c, _ := gin.CreateTestContext(httptest.NewRecorder())
    c.Request = httptest.NewRequest("POST", "/gardens", strings.NewReader(`{"name": "Test Garden"}`))
    c.Request.Header.Set("Content-Type", "application/json")

    input := &maes.CreateGardenMaeIn{}
    err := binder.Bind(context.Background(), input, c)

    assert.NoError(t, err)
    assert.Equal(t, "Test Garden", input.Request.Form.Name)
}

Binders provide the crucial first step in request processing, ensuring that raw HTTP data is properly structured and typed for use throughout the application.