node-engine client: a candidate's composition key is its own, over mesh-sdk keys; the client no longer imports go/delivery (ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -10,10 +10,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -56,7 +57,7 @@ var ErrUnknownVersion = errors.New("node-engine: unknown compose-candidate answe
|
||||
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
||||
|
||||
func checkAsk(a seat.ComposeCandidateInput) error {
|
||||
if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil {
|
||||
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
|
||||
return fmt.Errorf("node-engine: a compose-candidate ask names no candidate: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -89,7 +90,7 @@ func CheckAnswer(a seat.ComposeCandidateAnswer) error {
|
||||
if a.Node == "" || !nameOK.MatchString(a.Node) {
|
||||
return fmt.Errorf("node-engine: the answer's node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", a.Node)
|
||||
}
|
||||
if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil {
|
||||
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Took < 0 {
|
||||
@@ -181,3 +182,37 @@ func DecodeAnswer(data []byte) (seat.ComposeCandidateAnswer, error) {
|
||||
}
|
||||
return a, CheckAnswer(a)
|
||||
}
|
||||
|
||||
var heads12 = regexp.MustCompile(`^[0-9a-f]{12}$`)
|
||||
|
||||
// CompositionKey is the key of a candidate the node is asked to compose: the group's check at its heads, `<escaped
|
||||
// group>.<heads hash>`, the group escaped as a key part and the heads hash 12 lowercase hex digits (the checker's verdict
|
||||
// key of the same check).
|
||||
func CompositionKey(group, headsHash string) (string, error) {
|
||||
if group == "" {
|
||||
return "", errors.New("node-engine: the group name is empty")
|
||||
}
|
||||
if !heads12.MatchString(headsHash) {
|
||||
return "", fmt.Errorf("node-engine: %q is not a heads hash, 12 lowercase hex digits", headsHash)
|
||||
}
|
||||
return keys.EscapeName(group) + "." + headsHash, nil
|
||||
}
|
||||
|
||||
// ParseCompositionKey is the inverse of CompositionKey; it refuses a key that is not one, naming it.
|
||||
func ParseCompositionKey(key string) (group, headsHash string, err error) {
|
||||
i := strings.LastIndex(key, ".")
|
||||
if i < 0 {
|
||||
return "", "", fmt.Errorf("node-engine: %q is not a composition key, <group>.<heads hash>", key)
|
||||
}
|
||||
part, headsHash := key[:i], key[i+1:]
|
||||
if !heads12.MatchString(headsHash) {
|
||||
return "", "", fmt.Errorf("node-engine: %q is not a composition key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash)
|
||||
}
|
||||
if part == "" || strings.Contains(part, ".") {
|
||||
return "", "", fmt.Errorf("node-engine: %q is not a composition key: the group part is empty or holds a dot", key)
|
||||
}
|
||||
if group, err = keys.UnescapeName(part); err != nil {
|
||||
return "", "", fmt.Errorf("node-engine: %q is not a composition key: %w", key, err)
|
||||
}
|
||||
return group, headsHash, nil
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import (
|
||||
"testing"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could
|
||||
// not take, naming it.
|
||||
func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) {
|
||||
key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab")
|
||||
key, err := CompositionKey("mesh-sdk#7", "0123456789ab")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -95,3 +94,22 @@ func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A composition key round-trips its group and heads hash, and refuses what is not one.
|
||||
func TestCompositionKeyRoundTripsAndRefuses(t *testing.T) {
|
||||
key, err := CompositionKey("fix/1.2", "0123456789ab")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, h, err := ParseCompositionKey(key); err != nil || g != "fix/1.2" || h != "0123456789ab" {
|
||||
t.Fatalf("round trip: %q %q %v", g, h, err)
|
||||
}
|
||||
for _, bad := range []string{"", "nogroup", "g.0123456789AB", "g.short", ".0123456789ab", "a.b.0123456789ab"} {
|
||||
if _, _, err := ParseCompositionKey(bad); err == nil {
|
||||
t.Errorf("%q was taken for a composition key", bad)
|
||||
}
|
||||
}
|
||||
if _, err := CompositionKey("", "0123456789ab"); err == nil {
|
||||
t.Error("a key with no group was made")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user