summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 13:16:48 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 13:16:48 +0300
commitbbefcf38c5391c8338bb2edf205ab775e3a38f74 (patch)
treef03f45a71308c0d47c052a3afee7cccfef42656e
parent7078e12d89b13ee5858b9ade1ad63b23e7903dbc (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.go22
-rw-r--r--internal/rpn/operations.go4
-rw-r--r--internal/rpn/operations_metric_cmd.go42
-rw-r--r--internal/rpn/operations_metric_cmd_test.go86
-rw-r--r--internal/rpn/rpn_parse.go47
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