package apierrors import ( "encoding/json" "fmt" "go/token" "math" "reflect" "regexp" "strconv" "strings" ) // Parameter is ordered: declaration order is the generated function's argument // order. Example is documentation-only and never substitutes a missing value. type Parameter struct { Name string `json:"name"` Type string `json:"type"` Example any `json:"example,omitempty"` } type messagePart struct{ text, param string } var parameterName = regexp.MustCompile(`^[a-z][a-zA-Z0-9]*$`) // ParamError describes a programming/configuration error without including the // supplied value. It remains available through errors.As on the internal cause. type ParamError struct{ Code, Param, Problem string } func (e *ParamError) Error() string { return fmt.Sprintf("errors: %s parameter %q: %s", e.Code, e.Param, e.Problem) } // WithParams returns a detached error. It never retains the caller's map or any // mutable parameter value. Catalog-specific validation happens during Resolve. func (e *APIError) WithParams(values map[string]any) *APIError { out := e.Clone() out.params = make(map[string]any, len(values)) out.paramErr = nil for name, value := range values { scalar, err := copyScalar(value) if err != nil { out.paramErr = &ParamError{Code: out.Code, Param: name, Problem: "expected a scalar value"} continue } out.params[name] = scalar } return out } // Clone detaches request-specific parameters; Cause remains an error chain. func (e *APIError) Clone() *APIError { if e == nil { return nil } out := *e out.params = make(map[string]any, len(e.params)) for k, v := range e.params { out.params[k] = v } return &out } func copyScalar(v any) (any, error) { if n, ok := v.(json.Number); ok { return n, nil } if v == nil { return nil, fmt.Errorf("nil scalar") } r := reflect.ValueOf(v) switch r.Kind() { case reflect.String: return r.String(), nil case reflect.Bool: return r.Bool(), nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return r.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return r.Uint(), nil case reflect.Float32, reflect.Float64: return r.Float(), nil } return nil, fmt.Errorf("unsupported scalar") } func paramValue(kind string, v any) (any, error) { value, err := copyScalar(v) if err != nil { return nil, err } switch kind { case "string": if s, ok := value.(string); ok { return s, nil } case "bool": if b, ok := value.(bool); ok { return b, nil } case "int", "int32", "int64": var n int64 switch x := value.(type) { case int64: n = x case uint64: if x > math.MaxInt64 { return nil, fmt.Errorf("integer overflow") } n = int64(x) case json.Number: n, err = x.Int64() if err != nil { return nil, fmt.Errorf("expected exact integer") } default: return nil, fmt.Errorf("expected %s", kind) } if (kind == "int32" || kind == "int" && strconv.IntSize == 32) && (n < math.MinInt32 || n > math.MaxInt32) { return nil, fmt.Errorf("%s overflow", kind) } return n, nil case "float64": var n float64 switch x := value.(type) { case float64: n = x case json.Number: n, err = x.Float64() if err != nil { return nil, fmt.Errorf("expected finite float64") } default: return nil, fmt.Errorf("expected float64") } if math.IsNaN(n) || math.IsInf(n, 0) { return nil, fmt.Errorf("expected finite float64") } return n, nil } return nil, fmt.Errorf("expected %s", kind) } func compileMessage(d *Definition) ([]messagePart, error) { if d.Params == nil { return []messagePart{{text: d.Message}}, nil } if len(d.Params) == 0 { return nil, fmt.Errorf("params must be nonempty; omit params for a fixed message") } declared := map[string]bool{} for i, p := range d.Params { if !parameterName.MatchString(p.Name) || token.Lookup(p.Name).IsKeyword() || declared[p.Name] { return nil, fmt.Errorf("invalid or duplicate parameter %q", p.Name) } switch p.Type { case "string", "bool", "int", "int32", "int64", "float64": default: return nil, fmt.Errorf("unsupported parameter type %q", p.Type) } declared[p.Name] = true if p.Example != nil { example, err := paramValue(p.Type, p.Example) if err != nil { return nil, fmt.Errorf("example for %s: %w", p.Name, err) } d.Params[i].Example = example } } var parts []messagePart var literal strings.Builder used := map[string]bool{} flush := func() { if literal.Len() > 0 { parts = append(parts, messagePart{text: literal.String()}) literal.Reset() } } for i := 0; i < len(d.Message); { ch := d.Message[i] if ch != '{' && ch != '}' { literal.WriteByte(ch) i++ continue } if i+1 < len(d.Message) && d.Message[i+1] == ch { literal.WriteByte(ch) i += 2 continue } if ch == '}' { return nil, fmt.Errorf("unescaped closing brace") } end := strings.IndexByte(d.Message[i+1:], '}') if end < 0 { return nil, fmt.Errorf("unclosed placeholder") } name := d.Message[i+1 : i+1+end] if !declared[name] { return nil, fmt.Errorf("undeclared placeholder %q", name) } flush() parts = append(parts, messagePart{param: name}) used[name] = true i += end + 2 } flush() for _, p := range d.Params { if !used[p.Name] { return nil, fmt.Errorf("unused parameter %q", p.Name) } } return parts, nil } func (r *Registry) render(key string, d Definition, params map[string]any) (string, error) { values := make(map[string]any, len(d.Params)) for _, p := range d.Params { v, ok := params[p.Name] if !ok { return "", &ParamError{key, p.Name, "missing value"} } normalized, err := paramValue(p.Type, v) if err != nil { return "", &ParamError{key, p.Name, err.Error()} } values[p.Name] = normalized } for name := range params { if _, ok := values[name]; !ok { return "", &ParamError{key, name, "not declared"} } } var result strings.Builder for _, part := range r.messages[key] { if part.param != "" { fmt.Fprint(&result, values[part.param]) } else { result.WriteString(part.text) } } return result.String(), nil } // Example resolves documentation values using the same path as runtime errors. // Missing examples receive deterministic samples, never runtime defaults. func (r *Registry) Example(key string) *APIError { if r == nil { return r.Resolve(ByCode(key)) } d, ok := r.catalog.Definitions[key] if !ok { return r.Resolve(ByCode(key)) } values := map[string]any{} for _, p := range d.Params { v := p.Example if v == nil { switch p.Type { case "string": v = p.Name case "bool": v = false case "float64": v = float64(1) default: v = int64(1) } } values[p.Name] = v } return r.Resolve(ByCode(key).WithParams(values)) }