package component import ( "context" "fmt" "log/slog" "slices" "strings" ) type componentKey int const storageKey componentKey = iota // Component identifies a named unit within a module and its position in the call hierarchy. type Component interface { Module() string Path() []string PathString() string } type component struct { module string path []string } func (c component) Module() string { return c.module } func (c component) Path() []string { return slices.Clone(c.path) } func (c component) PathString() string { return strings.Join(c.path, ".") } func insert(ctx context.Context, module string, name string, path []string) context.Context { return context.WithValue(ctx, storageKey, component{ module: module, path: append(path, name), }) } // Get retrieves the current component from the context; returns false if none is present. func Get(ctx context.Context) (Component, bool) { t, ok := ctx.Value(storageKey).(component) return t, ok } // MustStart is Start but panics on error. func MustStart(ctx context.Context, module string, name string) context.Context { if ctx == nil { panic("context is nil") } if module == "" { panic("module cannot be empty") } if name == "" { panic("name cannot be empty") } return insert(ctx, module, name, []string{}) } // MustNext is Next but panics on error. func MustNext(ctx context.Context, name string) context.Context { if ctx == nil { panic("context is nil") } c, ok := Get(ctx) if !ok { panic("missing parent component") } if name == "" { panic("name cannot be empty") } return insert(ctx, c.Module(), name, c.Path()) } // Start sets a new component on the context, resetting any existing component. func Start(ctx context.Context, module string, name string) (context.Context, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } if module == "" { return nil, fmt.Errorf("module cannot be empty") } if name == "" { return nil, fmt.Errorf("name cannot be empty") } return insert(ctx, module, name, []string{}), nil } // Next appends name to the current component path, inheriting its module. func Next(ctx context.Context, name string) (context.Context, error) { if ctx == nil { return nil, fmt.Errorf("context is nil") } c, ok := Get(ctx) if !ok { return nil, fmt.Errorf("missing parent component") } if name == "" { return nil, fmt.Errorf("name cannot be empty") } return insert(ctx, c.Module(), name, c.Path()), nil } // GetFields returns the component as a string map with keys "module" and "path". func GetFields(ctx context.Context) (map[string]string, bool) { c, ok := Get(ctx) if !ok { return nil, false } return map[string]string{ "module": c.Module(), "path": c.PathString(), }, true } // Attrs returns the slog projection of GetFields. func Attrs(ctx context.Context) ([]slog.Attr, bool) { fields, ok := GetFields(ctx) if !ok { return nil, false } return []slog.Attr{ slog.String("module", fields["module"]), slog.String("path", fields["path"]), }, true }