package response import ( "bytes" "encoding/json" "errors" "fmt" "math" "reflect" "strings" ) // Config defines a JSON response template or a convenient flat envelope. // Body values can contain typed slots; ErrorBody optionally overrides errors. // Without Body, empty field names retain defaults and "-" disables a field. // This is application configuration, never a process-global response policy. type Config struct { Body map[string]any `json:"body"` ErrorBody map[string]any `json:"errorBody"` CodeField string `json:"codeField,omitempty"` MessageField string `json:"messageField,omitempty"` DataField string `json:"dataField,omitempty"` RequestIDField string `json:"requestIdField,omitempty"` CodeType string `json:"codeType,omitempty"` // string, int32, int64; integer is legacy SuccessCode any `json:"successCode,omitempty"` ErrorCode any `json:"errorCode,omitempty"` SuccessMessage string `json:"successMessage,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` DataMode string `json:"dataMode,omitempty"` Codes map[string]any `json:"codes,omitempty"` Messages map[string]string `json:"messages,omitempty"` } type Formatter struct{ config Config } // UnmarshalJSON preserves int64 values in interface fields, including mappings // and body constants. DecodeFile and ordinary json.Unmarshal share this path. func (c *Config) UnmarshalJSON(data []byte) error { type plain Config value := plain(*c) decoder := json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() decoder.DisallowUnknownFields() if err := decoder.Decode(&value); err != nil { return err } *c = Config(value) return nil } // New validates and detaches all configuration before serving requests. func New(c Config) (*Formatter, error) { if c.ErrorBody != nil && c.Body == nil { return nil, errors.New("response: errorBody requires body") } if c.Body != nil { if c.CodeField != "" || c.MessageField != "" || c.DataField != "" || c.RequestIDField != "" { return nil, errors.New("response: body cannot be combined with field-name shortcuts") } for _, body := range []map[string]any{c.Body, c.ErrorBody} { if err := validateTemplate(body, 0); err != nil { return nil, err } } } c = c.Clone() defaults := []struct { field *string value string }{{&c.CodeField, "code"}, {&c.MessageField, "message"}, {&c.DataField, "data"}, {&c.RequestIDField, "requestId"}, {&c.CodeType, "string"}, {&c.DataMode, "omitNil"}} for _, d := range defaults { if *d.field == "" { *d.field = d.value } } if c.CodeType != "string" && c.CodeType != "int32" && c.CodeType != "int64" && c.CodeType != "integer" { return nil, errors.New("response: codeType must be string, int32 or int64 (legacy integer is also accepted)") } if c.DataMode != "omitNil" && c.DataMode != "always" { return nil, errors.New("response: dataMode must be omitNil or always") } seen := map[string]bool{} for _, field := range []string{c.CodeField, c.MessageField, c.DataField, c.RequestIDField} { if field == "-" { continue } if strings.TrimSpace(field) != field || strings.ContainsAny(field, "\r\n\t") || seen[field] { return nil, errors.New("response: field names must be distinct nonempty names without surrounding whitespace") } seen[field] = true } if c.CodeType != "string" && (c.SuccessCode == nil || c.ErrorCode == nil) { return nil, errors.New("response: integer codes require successCode and errorCode") } for _, item := range []struct { name string value *any }{{"successCode", &c.SuccessCode}, {"errorCode", &c.ErrorCode}} { if *item.value == nil { continue } value, err := normalizeCode(*item.value, c.CodeType) if err != nil { return nil, fmt.Errorf("response: %s: %w", item.name, err) } *item.value = value } for name, value := range c.Codes { if strings.TrimSpace(name) == "" { return nil, errors.New("response: code mapping requires a logical code") } v, err := normalizeCode(value, c.CodeType) if err != nil { return nil, fmt.Errorf("response: codes.%s: %w", name, err) } c.Codes[name] = v } return &Formatter{config: c}, nil } func MustNew(c Config) *Formatter { f, err := New(c) if err != nil { panic(err) } return f } func (c Config) Validate() error { _, err := New(c); return err } func (c Config) Clone() Config { if c.Body != nil { c.Body = cloneTemplate(c.Body, 0).(map[string]any) } if c.ErrorBody != nil { c.ErrorBody = cloneTemplate(c.ErrorBody, 0).(map[string]any) } if c.Codes != nil { values := make(map[string]any, len(c.Codes)) for k, v := range c.Codes { values[k] = v } c.Codes = values } if c.Messages != nil { values := make(map[string]string, len(c.Messages)) for k, v := range c.Messages { values[k] = v } c.Messages = values } return c } func (f *Formatter) Config() Config { c := f.config.Clone() if c.Body != nil { c.CodeField, c.MessageField, c.DataField, c.RequestIDField = "", "", "", "" } return c } // Value is also used by documentation generation, keeping code/message mapping // and field omission identical to the actual response encoder. func (f *Formatter) Value(r Result) map[string]any { c := f.config code, message := any(r.Code), r.Message isError := r.IsError() if isError { if c.ErrorCode != nil { code = c.ErrorCode } if c.ErrorMessage != "" { message = c.ErrorMessage } } else { if c.SuccessCode != nil { code = c.SuccessCode } if c.SuccessMessage != "" { message = c.SuccessMessage } } if mapped, ok := c.Codes[r.Code]; ok { code = mapped } if mapped, ok := c.Messages[r.Code]; ok { message = mapped } if isError && r.BusinessCode != nil { code, message = r.BusinessCode, r.Message } body := c.Body if isError && c.ErrorBody != nil { body = c.ErrorBody } if body != nil { return renderTemplate(body, map[string]any{"code": code, "message": message, "data": r.Data, "requestId": r.RequestID, "ok": !isError, "status": r.Status}, c.DataMode).(map[string]any) } value := map[string]any{} if c.CodeField != "-" { value[c.CodeField] = code } if c.MessageField != "-" { value[c.MessageField] = message } if c.DataField != "-" && (r.Data != nil || c.DataMode == "always") { value[c.DataField] = r.Data } if c.RequestIDField != "-" && r.RequestID != "" { value[c.RequestIDField] = r.RequestID } return value } func (f *Formatter) Encode(r Result) (Payload, error) { if err := f.validateError(r); err != nil { return Payload{}, err } return JSON(r.Status, f.Value(r)) } func normalizeCode(value any, kind string) (any, error) { if kind == "string" { if s, ok := value.(string); ok && s != "" { return s, nil } return nil, errors.New("expected a nonempty string") } if value == nil { return nil, errors.New("expected an integer") } if n, ok := value.(json.Number); ok { v, err := n.Int64() if err != nil { return nil, errors.New("expected a signed 64-bit integer") } value = v } v := reflect.ValueOf(value) var n int64 switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n = v.Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if v.Uint() > math.MaxInt64 { return nil, errors.New("integer code exceeds int64 range") } n = int64(v.Uint()) case reflect.Float32, reflect.Float64: x := v.Float() if math.IsNaN(x) || math.IsInf(x, 0) || math.Trunc(x) != x || math.Abs(x) > 9007199254740991 { return nil, errors.New("expected an exact integer code") } n = int64(x) default: return nil, errors.New("expected an integer") } if kind == "int32" { if n < math.MinInt32 || n > math.MaxInt32 { return nil, errors.New("integer code exceeds int32 range") } return int32(n), nil } if kind == "integer" && (n < -9007199254740991 || n > 9007199254740991) { return nil, errors.New("integer code exceeds exact JSON range") } return n, nil }