diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-22 13:16:48 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-22 13:16:48 +0300 |
| commit | bbefcf38c5391c8338bb2edf205ab775e3a38f74 (patch) | |
| tree | f03f45a71308c0d47c052a3afee7cccfef42656e | |
| parent | 7078e12d89b13ee5858b9ade1ad63b23e7903dbc (diff) | |
feat: implement custom metric commands (define, undefine, list, show)
- Add Unregister() to MetricRegistry for removing custom metrics
- Handle 'custom show', 'custom list', 'custom define', 'custom undefine' in rpn_parse.go evaluate()
- Add CustomList, CustomDefine, CustomUndefine methods to Operations
- Update Operator interface with new custom metric methods
- Add comprehensive tests for all custom metric operations
| -rw-r--r-- | internal/rpn/metric_registry.go | 22 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 4 | ||||
| -rw-r--r-- | internal/rpn/operations_metric_cmd.go | 42 | ||||
| -rw-r--r-- | internal/rpn/operations_metric_cmd_test.go | 86 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 47 |
5 files changed, 201 insertions, 0 deletions
diff --git a/internal/rpn/metric_registry.go b/internal/rpn/metric_registry.go index de6b1a4..7a8b9aa 100644 --- a/internal/rpn/metric_registry.go +++ b/internal/rpn/metric_registry.go @@ -160,3 +160,25 @@ func (r *MetricRegistry) ListByCategory(cat Category) []*Metric { } return result } + +// Unregister removes a custom metric from the registry. +// Returns error if the metric is not custom (built-in metrics cannot be removed). +func (r *MetricRegistry) Unregister(name string) error { + r.mu.Lock() + defer r.mu.Unlock() + m, ok := r.metrics[name] + if !ok { + return fmt.Errorf("metric %q not found", name) + } + if !m.IsCustom { + return fmt.Errorf("cannot remove built-in metric %q", name) + } + delete(r.metrics, name) + // Also remove any aliases pointing to this metric + for alias, canonical := range r.aliases { + if canonical == name { + delete(r.aliases, alias) + } + } + return nil +} diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index d45ffd1..9e5bbca 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -165,6 +165,10 @@ type Operator interface { MetricList(stack *Stack) (string, error) MetricCategory(stack *Stack, categoryName string) (string, error) MetricCompatible(stack *Stack) (string, error) + // Custom metric commands + CustomList(stack *Stack) (string, error) + CustomDefine(name string, factor float64, category string) error + CustomUndefine(name string) error } // Operations provides operator implementations and stack manipulation. diff --git a/internal/rpn/operations_metric_cmd.go b/internal/rpn/operations_metric_cmd.go index 5ed5fc6..f461511 100644 --- a/internal/rpn/operations_metric_cmd.go +++ b/internal/rpn/operations_metric_cmd.go @@ -97,3 +97,45 @@ func parseCategory(name string) (Category, bool) { return 0, false } } + +// CustomList returns all custom metric names. +func (o *Operations) CustomList(stack *Stack) (string, error) { + reg := o.metricRegistry + metrics := reg.ListByCategory(Custom) + var names []string + for _, m := range metrics { + names = append(names, m.Name) + } + sort.Strings(names) + if len(names) == 0 { + return "no custom metrics defined", nil + } + return strings.Join(names, ", "), nil +} + +// CustomDefine registers a custom metric. +func (o *Operations) CustomDefine(name string, factor float64, category string) error { + reg := o.metricRegistry + // Check if already exists + if _, ok := reg.Find(name); ok { + return fmt.Errorf("metric %q already exists", name) + } + cat, ok := parseCategory(category) + if !ok { + return fmt.Errorf("unknown category %q", category) + } + m := &Metric{ + Name: name, + Category: cat, + BaseUnit: cat.String() + "_base", + Factor: func(PrefixMode) float64 { return factor }, + IsCustom: true, + } + reg.Register(m) + return nil +} + +// CustomUndefine removes a custom metric. +func (o *Operations) CustomUndefine(name string) error { + return o.metricRegistry.Unregister(name) +} diff --git a/internal/rpn/operations_metric_cmd_test.go b/internal/rpn/operations_metric_cmd_test.go index 298f8b3..e7307f7 100644 --- a/internal/rpn/operations_metric_cmd_test.go +++ b/internal/rpn/operations_metric_cmd_test.go @@ -380,3 +380,89 @@ func TestPrefixMode(t *testing.T) { }) } } + +func TestCustomDefineAndList(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + + // Define a custom metric + result, err := rpn.ParseAndEvaluate("custom define foobar 42 Custom") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(result, "defined") { + t.Errorf("expected 'defined' in result, got: %s", result) + } + + // List custom metrics + result, err = rpn.ParseAndEvaluate("custom list") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(result, "foobar") { + t.Errorf("expected 'foobar' in custom list, got: %s", result) + } + + // Use the custom metric + result, err = rpn.ParseAndEvaluate("10foobar 5 +") + if err != nil { + t.Fatalf("unexpected error using custom metric: %v", err) + } + + // Undefine + result, err = rpn.ParseAndEvaluate("custom undefine foobar") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(result, "removed") { + t.Errorf("expected 'removed' in result, got: %s", result) + } +} + +func TestCustomDefineDuplicate(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("custom define Cool 1 Universal") + if err == nil { + t.Error("expected error for duplicate metric name") + } +} + +func TestCustomDefineInvalidCategory(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("custom define foo 1 Nope") + if err == nil { + t.Error("expected error for invalid category") + } +} + +func TestCustomUndefineBuiltIn(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("custom undefine Cool") + if err == nil { + t.Error("expected error for undefining built-in metric") + } +} + +func TestCustomUndefineNotFound(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + _, err := rpn.ParseAndEvaluate("custom undefine nonexistent") + if err == nil { + t.Error("expected error for undefining non-existent metric") + } +} + +func TestCustomListEmpty(t *testing.T) { + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("custom list") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(result, "no custom") { + t.Errorf("expected 'no custom' when empty, got: %s", result) + } +} diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index ab64ec7..200b080 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -393,6 +393,53 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) { } } + // Handle multi-word custom command: custom <subcommand> + if token == "custom" && i+1 < len(tokens) { + subCmd := tokens[i+1] + switch subCmd { + case "show": + result, err := r.ops.MetricShow(stack) + if err != nil { + return "", fmt.Errorf("rpn: custom show: %w", err) + } + return result, nil + case "list": + result, err := r.ops.CustomList(stack) + if err != nil { + return "", fmt.Errorf("rpn: custom list: %w", err) + } + return result, nil + case "define": + if i+4 < len(tokens) { + name := tokens[i+2] + factorStr := tokens[i+3] + category := tokens[i+4] + factor, err := strconv.ParseFloat(factorStr, 64) + if err != nil { + return "", fmt.Errorf("rpn: custom define: invalid factor %q", factorStr) + } + err = r.ops.CustomDefine(name, factor, category) + if err != nil { + return "", fmt.Errorf("rpn: custom define: %w", err) + } + return fmt.Sprintf("defined custom metric %q (factor: %g, category: %s)", name, factor, category), nil + } + return "", fmt.Errorf("rpn: custom define: usage: custom define <name> <factor> <category>") + case "undefine": + if i+2 < len(tokens) { + name := tokens[i+2] + err := r.ops.CustomUndefine(name) + if err != nil { + return "", fmt.Errorf("rpn: custom undefine: %w", err) + } + return fmt.Sprintf("removed custom metric %q", name), nil + } + return "", fmt.Errorf("rpn: custom undefine: usage: custom undefine <name>") + default: + return "", fmt.Errorf("rpn: unknown custom subcommand %q. Use: show, list, define, undefine", subCmd) + } + } + // Check if this is a variable name for assignment (:= or =:) // For := (right assignment): name value := - first token is always a variable name // For =: (left assignment): value name =: - token before =: is a variable name |
