v1.0.0 — first release

Bump VERSION to 1.0.0 and cut the first tagged release of vpp-maglev.

Also in this commit:

- maglevc: MAGLEV_SERVER env var as an alternative to the --server
  flag, matching the MAGLEV_CONFIG / MAGLEV_GRPC_ADDR convention on
  the other binaries. The flag takes precedence when both are set.
- Rename cmd/maglevd -> cmd/server and cmd/maglevc -> cmd/client so
  the source directory names are decoupled from binary names (the
  frontend and tester commands already followed this convention).
  Build outputs and the Debian packages are unchanged.
This commit is contained in:
2026-04-15 15:23:46 +02:00
parent 177d81cca1
commit bc6ccaa844
15 changed files with 33 additions and 17 deletions

168
cmd/client/tree.go Normal file
View File

@@ -0,0 +1,168 @@
// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import (
"context"
"strings"
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
)
// Node is one word in the command tree. Leaf nodes have a Run function.
// Slot nodes have Dynamic set (and no fixed Word to match against); they
// accept any single token as an argument and may have further Children.
type Node struct {
Word string
Help string
Dynamic func(context.Context, grpcapi.MaglevClient) []string // non-nil → slot node
Children []*Node
Run func(context.Context, grpcapi.MaglevClient, []string) error
}
// Walk descends the tree following tokens. At each step it tries fixed
// children first (exact then prefix), then falls back to a slot child
// (Dynamic != nil). Tokens consumed by slot children are collected as args.
// Returns the deepest node reached, the args collected from slot nodes,
// and any tokens that could not be matched. A non-empty remaining slice
// means the input contained a token that neither matched a fixed child
// at the current node nor fed into a slot — callers should treat that
// as "unknown command" rather than silently anchoring help at the root.
func Walk(root *Node, tokens []string) (*Node, []string, []string) {
node := root
var args []string
for len(tokens) > 0 {
tok := tokens[0]
// Try fixed children (exact, then unique prefix).
next := matchFixedChild(node.Children, tok)
if next != nil {
node = next
tokens = tokens[1:]
continue
}
// Try a slot child.
slot := findSlotChild(node.Children)
if slot != nil {
args = append(args, tok)
tokens = tokens[1:]
node = slot
continue
}
// Dead end — no match. The caller gets the still-unconsumed tail
// in the third return value.
break
}
return node, args, tokens
}
// matchFixedChild returns the child matching tok by exact then unique prefix,
// considering only non-slot children.
func matchFixedChild(children []*Node, tok string) *Node {
var fixed []*Node
for _, c := range children {
if c.Dynamic == nil {
fixed = append(fixed, c)
}
}
// Exact match.
for _, c := range fixed {
if c.Word == tok {
return c
}
}
// Unique prefix match.
var matches []*Node
for _, c := range fixed {
if strings.HasPrefix(c.Word, tok) {
matches = append(matches, c)
}
}
if len(matches) == 1 {
return matches[0]
}
return nil
}
// findSlotChild returns the first child that is a slot node (Dynamic != nil).
func findSlotChild(children []*Node) *Node {
for _, c := range children {
if c.Dynamic != nil {
return c
}
}
return nil
}
// helpLine is a (path, help) pair used when displaying '?' output.
type helpLine struct {
path string
help string
}
// expandPaths returns all (path, help) pairs for every node reachable from
// node that has a Run function. prefix is the display string accumulated so
// far (e.g. "show frontend"). visited prevents infinite loops through
// self-referencing slot nodes like watchEventsOptSlot.
func expandPaths(node *Node, prefix string, visited map[*Node]bool) []helpLine {
if visited[node] {
return nil
}
visited[node] = true
var lines []helpLine
if node.Run != nil {
lines = append(lines, helpLine{path: prefix, help: node.Help})
}
for _, child := range node.Children {
childPrefix := child.Word
if prefix != "" {
childPrefix = prefix + " " + child.Word
}
lines = append(lines, expandPaths(child, childPrefix, visited)...)
}
return lines
}
// Candidates returns the completable children at the current position given
// the already-typed tokens and the partial token being completed.
func Candidates(root *Node, tokens []string, partial string, ctx context.Context, client grpcapi.MaglevClient) []*Node {
// Walk the already-confirmed tokens. If any of them are unknown,
// offer no completions at all — continuing to suggest children off
// the partially-walked node would mislead the user into "completing"
// an invalid command.
node, _, remaining := Walk(root, tokens)
if len(remaining) > 0 {
return nil
}
// Now look at what could follow at this node.
// Check fixed children filtered by partial.
fixedMatches := filterFixedChildren(node.Children, partial)
// Check dynamic slot child if present.
var dynMatches []*Node
slot := findSlotChild(node.Children)
if slot != nil && slot.Dynamic != nil {
vals := slot.Dynamic(ctx, client)
for _, v := range vals {
if strings.HasPrefix(v, partial) {
dynMatches = append(dynMatches, &Node{Word: v, Help: slot.Help})
}
}
}
return append(fixedMatches, dynMatches...)
}
func filterFixedChildren(children []*Node, prefix string) []*Node {
var out []*Node
for _, c := range children {
if c.Dynamic == nil && strings.HasPrefix(c.Word, prefix) {
out = append(out, c)
}
}
return out
}