// Package client provides a Go SDK for interacting with the MapleFile API. package client import ( "encoding/json" "fmt" "strings" ) // ProblemDetail represents an RFC 9457 problem detail response from the API. type ProblemDetail struct { Type string `json:"type"` Status int `json:"status"` Title string `json:"title"` Detail string `json:"detail,omitempty"` Instance string `json:"instance,omitempty"` Errors map[string]string `json:"errors,omitempty"` Timestamp string `json:"timestamp"` TraceID string `json:"trace_id,omitempty"` } // APIError wraps ProblemDetail for the error interface. type APIError struct { ProblemDetail } // Error returns a formatted error message from the ProblemDetail. func (e *APIError) Error() string { var errMsg strings.Builder if e.Detail != "" { errMsg.WriteString(e.Detail) } else { errMsg.WriteString(e.Title) } if len(e.Errors) > 0 { errMsg.WriteString("\n\nValidation errors:") for field, message := range e.Errors { errMsg.WriteString(fmt.Sprintf("\n - %s: %s", field, message)) } } return errMsg.String() } // StatusCode returns the HTTP status code from the error. func (e *APIError) StatusCode() int { return e.Status } // GetValidationErrors returns the validation errors map. func (e *APIError) GetValidationErrors() map[string]string { return e.Errors } // GetFieldError returns the error message for a specific field, or empty string if not found. func (e *APIError) GetFieldError(field string) string { if e.Errors == nil { return "" } return e.Errors[field] } // HasFieldError checks if a specific field has a validation error. func (e *APIError) HasFieldError(field string) bool { if e.Errors == nil { return false } _, exists := e.Errors[field] return exists } // IsNotFound checks if the error is a 404 Not Found error. func IsNotFound(err error) bool { if apiErr, ok := err.(*APIError); ok { return apiErr.Status == 404 } return false } // IsUnauthorized checks if the error is a 401 Unauthorized error. func IsUnauthorized(err error) bool { if apiErr, ok := err.(*APIError); ok { return apiErr.Status == 401 } return false } // IsForbidden checks if the error is a 403 Forbidden error. func IsForbidden(err error) bool { if apiErr, ok := err.(*APIError); ok { return apiErr.Status == 403 } return false } // IsValidationError checks if the error has validation errors. func IsValidationError(err error) bool { if apiErr, ok := err.(*APIError); ok { return len(apiErr.Errors) > 0 } return false } // IsConflict checks if the error is a 409 Conflict error. func IsConflict(err error) bool { if apiErr, ok := err.(*APIError); ok { return apiErr.Status == 409 } return false } // IsTooManyRequests checks if the error is a 429 Too Many Requests error. func IsTooManyRequests(err error) bool { if apiErr, ok := err.(*APIError); ok { return apiErr.Status == 429 } return false } // parseErrorResponse attempts to parse an error response body into an APIError. // It tries RFC 9457 format first, then falls back to legacy format. // // Note: RFC 9457 specifies that error responses should use Content-Type: application/problem+json, // but we parse based on the response structure rather than Content-Type for maximum compatibility. func parseErrorResponse(body []byte, statusCode int) error { // Try to parse as RFC 9457 ProblemDetail // The presence of the "type" field distinguishes RFC 9457 from legacy responses var problem ProblemDetail if err := json.Unmarshal(body, &problem); err == nil && problem.Type != "" { return &APIError{ProblemDetail: problem} } // Fallback for non-RFC 9457 errors var errorResponse map[string]interface{} if err := json.Unmarshal(body, &errorResponse); err == nil { if errMsg, ok := errorResponse["message"].(string); ok { return &APIError{ ProblemDetail: ProblemDetail{ Status: statusCode, Title: errMsg, Detail: errMsg, }, } } } // Last resort: return raw body as error return &APIError{ ProblemDetail: ProblemDetail{ Status: statusCode, Title: fmt.Sprintf("HTTP %d", statusCode), Detail: string(body), }, } }