fields.go

// slog-style attribute helpers. These produce values that Logger.With and
// Logger.WithFields accept; the helpers exist mainly to make call sites read
// better and to centralise the (relatively few) type coercions we perform.
package lambdalog

// Attr is a named value suitable for inclusion on a log record. It maps 1:1
// onto the internal attr type but is exported so callers can build slices of
// attributes ahead of time and pass them as a group.
type Attr struct {
	Key   string
	Value any
}

// String builds an attribute with a string value.
func String(key, value string) Attr { return Attr{Key: key, Value: value} }

// Int builds an attribute with an int value. For callers working with int64
// directly use Int64 to avoid the widening copy.
func Int(key string, value int) Attr { return Attr{Key: key, Value: value} }

// Int64 builds an attribute with an int64 value.
func Int64(key string, value int64) Attr { return Attr{Key: key, Value: value} }

// Bool builds an attribute with a boolean value.
func Bool(key string, value bool) Attr { return Attr{Key: key, Value: value} }

// Err builds an attribute named "err" from the given error. If err is nil
// the Value is set to nil so downstream encoding emits JSON null.
func Err(err error) Attr {
	if err == nil {
		return Attr{Key: "err", Value: nil}
	}
	return Attr{Key: "err", Value: err.Error()}
}

// Group builds a nested attribute: the key maps to a flattened map whose
// entries are the nested attrs. Groups nest: Group("outer", Group("inner",
// String("k", "v"))) produces {"outer":{"inner":{"k":"v"}}}.
func Group(key string, attrs ...Attr) Attr {
	return Attr{Key: key, Value: flattenGroup(attrs)}
}

func flattenGroup(attrs []Attr) map[string]any {
	out := make(map[string]any, len(attrs))
	for _, a := range attrs {
		out[a.Key] = a.Value
	}
	return out
}

// Merge combines two attribute slices into a new one. The right-hand slice
// wins on key collisions. The returned slice is always a new allocation so
// callers can mutate either input afterwards.
func Merge(left, right []Attr) []Attr {
	out := make([]Attr, 0, len(left)+len(right))
	seen := make(map[string]int, len(left)+len(right))
	for _, a := range left {
		seen[a.Key] = len(out)
		out = append(out, a)
	}
	for _, a := range right {
		if idx, ok := seen[a.Key]; ok {
			out[idx] = a
			continue
		}
		seen[a.Key] = len(out)
		out = append(out, a)
	}
	return out
}

// Copy returns a deep-enough copy of the attribute slice that the caller
// can mutate the result without aliasing the input. Nested groups are
// copied one level deep because that is the depth Group produces.
func Copy(attrs []Attr) []Attr {
	out := make([]Attr, len(attrs))
	for i, a := range attrs {
		if g, ok := a.Value.(map[string]any); ok {
			clone := make(map[string]any, len(g))
			for k, v := range g {
				clone[k] = v
			}
			out[i] = Attr{Key: a.Key, Value: clone}
			continue
		}
		out[i] = a
	}
	return out
}

// WithAttrs returns a child logger carrying every attribute in attrs. The
// parent logger's attribute slice is copied, matching the semantics of With.
func (l *Logger) WithAttrs(attrs ...Attr) *Logger {
	c := l.clone()
	for _, a := range attrs {
		c.attrs = append(c.attrs, attr{Key: a.Key, Value: a.Value})
	}
	return c
}

// asPairs flattens an Attr slice into the variadic ...any form expected by
// the Debug/Info/Warn/Error methods. Useful when callers build attributes
// programmatically and want to hand them to a single log call.
func asPairs(attrs []Attr) []any {
	out := make([]any, 0, 2*len(attrs))
	for _, a := range attrs {
		out = append(out, a.Key, a.Value)
	}
	return out
}