// Package calladdress reads the key a module calls a tool or a seat verb by, as a module's invokes names it, with the // machine it is asked of after '@': `seat:.[@]` or `.[@]` (novox/hq ADR 0159, // ADR 0469 ยง3). Every caller in the SDK parses its key here before it publishes anything, so a malformed key fails at // once and says why, never as a call nobody serves and a timeout (novox/hq issue 774, the lease write's stop asked on // `...call.stop@novox-be...`). package calladdress import ( "fmt" "strings" ) // Address is a parsed key: a seat and its verb, or a module and its tool, and the machine named, if any. type Address struct { Seat, Verb string Module, Tool string Node string } // Key is the address as a module's invokes names it, without the machine. func (a Address) Key() string { if a.Seat != "" { return "seat:" + a.Seat + "." + a.Verb } return a.Module + "." + a.Tool } // Parse reads a key, refusing one with more than one '@', a node that is not one subject token, or a seat, verb, module // or tool that holds a wildcard, white space or (but a tool) a dot. func Parse(key string) (Address, error) { name, node, err := cutNode(key) if err != nil { return Address{}, err } if rest, ok := strings.CutPrefix(name, "seat:"); ok { seat, verb, ok := strings.Cut(rest, ".") if !ok || seat == "" || verb == "" { return Address{}, fmt.Errorf("%q names no seat and verb (seat:.)", key) } if err := tokens(key, seat, verb); err != nil { return Address{}, err } return Address{Seat: seat, Verb: verb, Node: node}, nil } module, tool, ok := strings.Cut(name, ".") if !ok || module == "" || tool == "" { return Address{}, fmt.Errorf("%q is not ., and does not start seat:.", key) } if err := tokens(key, module); err != nil { return Address{}, err } if err := checkTool(key, tool); err != nil { return Address{}, err } return Address{Module: module, Tool: tool, Node: node}, nil } // CheckOwn is Parse for a caller that may also name a tool of its own module by its bare name (the runtime's mesh/ask). func CheckOwn(key string) error { name, _, err := cutNode(key) if err != nil { return err } if !strings.HasPrefix(name, "seat:") && !strings.Contains(name, ".") { return tokens(key, name) } _, err = Parse(key) return err } // CheckNode refuses a node name that is not one subject token. func CheckNode(node string) error { if node == "" || strings.ContainsAny(node, ".*>@ \t\r\n") { return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>', '@' or space)", node) } return nil } func cutNode(key string) (name, node string, err error) { name, node, at := strings.Cut(key, "@") if !at { return key, "", nil } if strings.Contains(node, "@") { return "", "", fmt.Errorf("%q holds more than one '@': a key names at most one machine", key) } if err := CheckNode(node); err != nil { return "", "", fmt.Errorf("%q: %w", key, err) } return name, node, nil } func tokens(key string, parts ...string) error { for _, p := range parts { if p == "" || strings.ContainsAny(p, ".*> \t\r\n") { return fmt.Errorf("%q: %q is not one subject token (no '.', '*', '>' or space)", key, p) } } return nil } // checkTool refuses a tool name with a wildcard or white space; a dot is kept, as the tool runner serves it. func checkTool(key, tool string) error { if strings.ContainsAny(tool, "*> \t\r\n") || strings.HasPrefix(tool, ".") || strings.HasSuffix(tool, ".") { return fmt.Errorf("%q: %q is not one subject token (no '*', '>' or space)", key, tool) } return nil }