mesh-catalog's client: the manifest digest and key, the modules value's rules, the verdict's rules and the candidate's fingerprint set, over the seat's typed states (ADR 0625 §4, ADR 0634 §2; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:04:06 +01:00
parent 6fb4236eb4
commit 6f22665c98
6 changed files with 573 additions and 1 deletions
@@ -0,0 +1,38 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// FingerprintSet is the `fpset` of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines
// `<module>=<fp2>` or `<module>=none:<reason>` of every module with an entry in a pull's fps, sorted and joined by a newline,
// so it is the same for every order and changes with any fingerprint or reason. A manifest digest does not enter: the
// fingerprint already covers the manifest. It is the empty string when no pull request carries a fingerprint. The checker
// keys its verdict by it.
func FingerprintSet(pulls []seat.CandidatesByGroupValuePullsItem) string {
lines := map[string]bool{}
for _, p := range pulls {
for module, m := range p.Fps {
if m.Fp2 != "" {
lines[module+"="+m.Fp2] = true
} else {
lines[module+"=none:"+m.None2] = true
}
}
}
if len(lines) == 0 {
return ""
}
all := make([]string, 0, len(lines))
for l := range lines {
all = append(all, l)
}
sort.Strings(all)
sum := sha256.Sum256([]byte(strings.Join(all, "\n")))
return hex.EncodeToString(sum[:])[:12]
}
@@ -0,0 +1,151 @@
package meshcatalogclient
import (
"errors"
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func hex64(c string) string { return strings.Repeat(c, 64) }
// The digest and the candidate's fingerprint set equal what the trunk's go/delivery computed for the same input (the
// expected strings are its output), so a key or a verdict written before is read after.
func TestDigestAndFingerprintSetKeepTheirForms(t *testing.T) {
if got := ManifestDigest([]byte(`{"module":"nats"}`)); got != "19a7450f557153c318558acac91cd6972321a192f6f95c3698a0e4c57edd4359" {
t.Errorf("ManifestDigest = %s", got)
}
pull := func(fps map[string]seat.CandidatesByGroupValuePullsItemFpsValue) []seat.CandidatesByGroupValuePullsItem {
return []seat.CandidatesByGroupValuePullsItem{{Fps: fps}}
}
fps := map[string]seat.CandidatesByGroupValuePullsItemFpsValue{"nats": {Fp2: SourcePrefix2 + hex64("a")}, "zsh": {None2: "no manifest"}}
set := FingerprintSet(pull(fps))
if set != "50a3f57cc4e2" {
t.Errorf("FingerprintSet = %s", set)
}
fps["zsh"] = seat.CandidatesByGroupValuePullsItemFpsValue{None2: "another reason"}
if FingerprintSet(pull(fps)) == set {
t.Error("a changed reason leaves the set as it was")
}
if FingerprintSet(pull(nil)) != "" {
t.Error("no fingerprint gives a set")
}
}
// A manifest and a module are read back as written, and each rule of the old values is kept: a refusal names its reason.
func TestManifestAndModuleValuesKeepTheirRules(t *testing.T) {
if _, err := ManifestsKey("abc"); err == nil || !strings.Contains(err.Error(), `"abc"`) {
t.Errorf("ManifestsKey(abc) = %v", err)
}
if k, err := ParseManifestsKey(hex64("b")); err != nil || k != hex64("b") {
t.Errorf("ParseManifestsKey = %q, %v", k, err)
}
raw, err := EncodeManifest(seat.ManifestsByDigestValue{Module: "nats", Manifest: `{"module":"nats"}`})
if err != nil {
t.Fatal(err)
}
if m, err := DecodeManifest(raw); err != nil || m.Module != "nats" || m.V != keys.Version {
t.Errorf("DecodeManifest = %+v, %v", m, err)
}
for _, bad := range []seat.ManifestsByDigestValue{{Module: "a.b", Manifest: "{}"}, {Module: "nats"}, {Module: "nats", Manifest: "not json"}, {Module: "nats", Manifest: "{\"a\":\"\xff\"}"}} {
if _, err := EncodeManifest(bad); err == nil {
t.Errorf("EncodeManifest(%+v) is accepted", bad)
}
}
if _, err := DecodeManifest([]byte(`{"module":"nats","manifest":"{}"}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("a manifest with no version: %v", err)
}
good := seat.ModulesByModuleValue{Fp2: SourcePrefix2 + hex64("a"), Repository: "novox/mesh-catalog", Commit: "c", Manifest: hex64("b"), Path: RootPath}
raw, err = EncodeModule(good)
if err != nil {
t.Fatal(err)
}
if m, err := DecodeModule(raw); err != nil || m.Fp2 != good.Fp2 || m.Path != RootPath {
t.Errorf("DecodeModule = %+v, %v", m, err)
}
refused := good
refused.Verdict, refused.Problems = VerdictRefused, []string{"p"}
if _, err := EncodeModule(refused); err != nil {
t.Errorf("a refused module: %v", err)
}
for name, mutate := range map[string]func(m *seat.ModulesByModuleValue){
"neither a fingerprint nor a reason": func(m *seat.ModulesByModuleValue) { m.Fp2 = "" },
"both fp2 and none2": func(m *seat.ModulesByModuleValue) { m.None2 = "x" },
"an old fingerprint of another form": func(m *seat.ModulesByModuleValue) { m.Fp = "src2:" + hex64("a") },
"a fingerprint of the old key form": func(m *seat.ModulesByModuleValue) { m.Fp2 = "src1:" + hex64("a") },
"inputs and no manifest": func(m *seat.ModulesByModuleValue) {
m.Manifest = ""
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t"}
},
"a refusal with no problem": func(m *seat.ModulesByModuleValue) { m.Verdict = VerdictRefused },
"problems with no refusal": func(m *seat.ModulesByModuleValue) { m.Problems = []string{"p"} },
"unknown fields with no refusal": func(m *seat.ModulesByModuleValue) { m.Unknown = []string{"series"} },
"a path outside the directory": func(m *seat.ModulesByModuleValue) { m.Paths = map[string][]string{"tools": {"../x"}} },
"a toolchain with no recipe": func(m *seat.ModulesByModuleValue) {
m.Toolchains = map[string]seat.ToolchainUsed{"go": {Language: "go", Version: "1.26"}}
},
"an import form with no imports": func(m *seat.ModulesByModuleValue) {
m.Inputs2 = &seat.ModulesByModuleValueInputs2{Tree: "t", ImportForm: SourcePrefix2 + hex64("a")}
},
} {
m := good
mutate(&m)
if _, err := EncodeModule(m); err == nil {
t.Errorf("%s: EncodeModule accepts %+v", name, m)
}
}
}
// A verdict is taken as the readers have always taken it: a missing one accepted, a refusal held with its problems, a wait
// held with the fields it waits on, and a module's own verdict field gives the same.
func TestVerdictsAreTakenAsTheReadersTookThem(t *testing.T) {
if refused, problems := TakeVerdict(nil); refused || problems != nil {
t.Errorf("no verdict: %v %v", refused, problems)
}
waits := seat.VerdictsByDigestValue{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"series"}, Judge: "j"}
refused, problems := TakeVerdict(&waits)
if !refused || len(problems) != 1 || problems[0] != "it carries series, which the indexer's judge does not know yet" {
t.Errorf("a wait: %v %v", refused, problems)
}
if got := VerdictWhy(waits); got != "the indexer's judge (j) holds the manifest of nats: "+problems[0] {
t.Errorf("VerdictWhy(wait) = %q", got)
}
if as := VerdictAsRefused(waits); as.State != VerdictRefused || len(as.Unknown) != 0 || as.Problems[0] != problems[0] {
t.Errorf("VerdictAsRefused = %+v", as)
}
no := seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "j"}
if got := VerdictWhy(no); got != "the indexer's judge (j) refused the manifest of nats: a; b" {
t.Errorf("VerdictWhy(refused) = %q", got)
}
if !SameVerdict(no, seat.VerdictsByDigestValue{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "k"}) {
t.Error("another judge is another verdict")
}
fromModule := ModuleVerdict("nats", seat.ModulesByModuleValue{Verdict: VerdictRefused, Unknown: []string{"series"}, Problems: []string{"x"}})
if fromModule == nil || fromModule.State != VerdictManifestWaits || fromModule.Unknown[0] != "series" {
t.Errorf("a refusal naming unknown fields is the wait it was written as: %+v", fromModule)
}
if ModuleVerdict("nats", seat.ModulesByModuleValue{}) != nil {
t.Error("a module with no verdict carries one")
}
for _, bad := range []seat.VerdictsByDigestValue{{Module: "nats", State: "accepted", Problems: []string{"p"}, Judge: "j"}, {Module: "nats", State: "refused", Judge: "j"}, {Module: "nats", State: "accepted"}, {Module: "nats", State: "odd", Judge: "j"}} {
if _, err := EncodeVerdict(bad); err == nil {
t.Errorf("EncodeVerdict(%+v) is accepted", bad)
}
}
raw, err := EncodeVerdict(no)
if err != nil {
t.Fatal(err)
}
if v, err := DecodeVerdict(raw); err != nil || !SameVerdict(v, no) {
t.Errorf("DecodeVerdict = %+v, %v", v, err)
}
if k, err := VerdictsKey(hex64("c")); err != nil || k != hex64("c") {
t.Errorf("VerdictsKey = %q %v", k, err)
}
if _, err := ModulesKey("a.b"); err == nil {
t.Error("ModulesKey accepts a dotted name")
}
}
@@ -2,7 +2,10 @@ module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
)
require (
github.com/klauspost/compress v1.20.1 // indirect
@@ -0,0 +1,79 @@
package meshcatalogclient
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"unicode/utf8"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ManifestDigest is the hex sha256 of a manifest's bytes exactly as read at the commit: the key of a `manifests` value and
// the digest a `modules` value and a candidate's module carry (novox/hq ADR 0429 clause 16).
func ManifestDigest(raw []byte) string {
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
// ManifestsKey is the key of a `manifests` value: the manifest's digest, 64 lowercase hex digits. The indexer writes it
// once, by a create that fails when the key exists.
func ManifestsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.ManifestsByDigestKey(digest), nil
}
// ParseManifestsKey is the inverse of ManifestsKey.
func ParseManifestsKey(key string) (string, error) {
digest, err := ManifestsKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifests key: %w", key, err)
}
return digest, nil
}
func checkManifest(m seat.ManifestsByDigestValue) error {
if err := keys.CheckName("module", m.Module); err != nil {
return err
}
if m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: the manifest of %s is empty", m.Module)
}
if !utf8.ValidString(m.Manifest) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not UTF-8 text, which a value cannot hold byte for byte", m.Module)
}
if !json.Valid([]byte(m.Manifest)) {
return fmt.Errorf("meshcatalogclient: the manifest of %s is not JSON", m.Module)
}
return nil
}
// EncodeManifest is m as JSON at the kept values' version; it refuses a module that is not a name and a manifest that is
// empty, not UTF-8 or not JSON.
func EncodeManifest(m seat.ManifestsByDigestValue) ([]byte, error) {
if err := checkManifest(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeManifest reads a `manifests` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeManifest
// refuses.
func DecodeManifest(data []byte) (seat.ManifestsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ManifestsByDigestValue{}, err
}
var m seat.ManifestsByDigestValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ManifestsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkManifest(m); err != nil {
return seat.ManifestsByDigestValue{}, err
}
return m, nil
}
@@ -0,0 +1,133 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"path"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// RootPath is a `modules` value's path when the module's manifest is its repository's root.
const RootPath = "."
// SourcePrefix1 opens the fingerprint of the old form a `modules` value may still carry in `fp`.
const SourcePrefix1 = "src1:"
// ModulesKey is the key of a `modules` value: the module's name.
func ModulesKey(module string) (string, error) {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
return seat.ModulesByModuleKey(module), nil
}
// ParseModulesKey is the inverse of ModulesKey.
func ParseModulesKey(key string) (string, error) {
module, err := ModulesKey(key)
if err != nil {
return "", fmt.Errorf("meshcatalogclient: %q is not a modules key: %w", key, err)
}
return module, nil
}
// CheckModule refuses a `modules` value that contradicts itself: both or neither of a fingerprint and the reason for none,
// of either form, an old fingerprint not of the form `src1:`, kept inputs without a manifest digest, a verdict the indexer
// does not write, problems without a refusal (or a refusal without problems), a path that is not clean inside the module's
// directory, and a fingerprint or a digest not of its form.
func CheckModule(m seat.ModulesByModuleValue) error {
switch {
case m.Fp != "" && m.None != "":
return fmt.Errorf("meshcatalogclient: a module has both a fingerprint and a reason for none")
case m.Fp == "" && m.None == "" && m.Fp2 == "" && m.None2 == "":
return fmt.Errorf("meshcatalogclient: a module has neither a fingerprint nor a reason for none, of either form")
case m.Fp != "" && !strings.HasPrefix(m.Fp, SourcePrefix1):
return fmt.Errorf("meshcatalogclient: the module's fingerprint %q does not start with %s", m.Fp, SourcePrefix1)
case m.Fp2 != "" && m.None2 != "":
return fmt.Errorf("meshcatalogclient: a module has both fp2 and none2")
}
for language, t := range m.Toolchains {
if t.Version == "" || t.Recipe == "" {
return fmt.Errorf("meshcatalogclient: the module's toolchain of %q needs its version and its recipe version", language)
}
}
if m.Inputs2 != nil && m.Manifest == "" {
return fmt.Errorf("meshcatalogclient: a module keeps new-form inputs but has no manifest digest")
}
for artifact, paths := range m.Paths {
if artifact == "" || len(paths) == 0 {
return fmt.Errorf("meshcatalogclient: the module's named paths of the artifact %q are empty or name no artifact", artifact)
}
for _, p := range paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the artifact %q names the path %q, not a clean path inside the module's directory", artifact, p)
}
}
}
if in := m.Inputs2; in != nil {
if f := in.ImportForm; f != "" && (len(in.Imports) == 0 || !strings.HasPrefix(f, SourcePrefix2) || !keys.IsHex64(f[len(SourcePrefix2):])) {
return fmt.Errorf("meshcatalogclient: the kept import form %q is set only with imports, and is %s and 64 hex digits", f, SourcePrefix2)
}
for p, object := range in.Paths {
if !cleanInside(p) {
return fmt.Errorf("meshcatalogclient: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("meshcatalogclient: the named path %q names no git object", p)
}
}
}
switch {
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
case len(m.Unknown) > 0 && m.Verdict != VerdictRefused:
return fmt.Errorf("meshcatalogclient: a module's unknown fields are named only with the refusal a wait is written as")
case m.Verdict != "" && m.Manifest == "":
return fmt.Errorf("meshcatalogclient: a module carries a verdict and no manifest digest")
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
return fmt.Errorf("meshcatalogclient: a module's problems are named only with a refusal, and a refusal names them")
case m.Verdict == VerdictRefused && len(m.Warnings) > 0:
return fmt.Errorf("meshcatalogclient: a refused module warns: a refusal names problems")
}
if m.Fp2 != "" {
if _, err := KeyPart(m.Fp2); err != nil {
return err
}
}
if m.Manifest != "" {
if _, err := ManifestsKey(m.Manifest); err != nil {
return err
}
}
return nil
}
func cleanInside(p string) bool {
return p != "" && p == path.Clean(p) && !strings.HasPrefix(p, "/") && p != ".." && !strings.HasPrefix(p, "../")
}
// EncodeModule is m as JSON at the kept values' version; it refuses what CheckModule refuses.
func EncodeModule(m seat.ModulesByModuleValue) ([]byte, error) {
if err := CheckModule(m); err != nil {
return nil, err
}
m.V = keys.Version
return json.Marshal(m)
}
// DecodeModule reads a `modules` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeModule refuses.
func DecodeModule(data []byte) (seat.ModulesByModuleValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.ModulesByModuleValue{}, err
}
var m seat.ModulesByModuleValue
if err := json.Unmarshal(data, &m); err != nil {
return seat.ModulesByModuleValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := CheckModule(m); err != nil {
return seat.ModulesByModuleValue{}, err
}
return m, nil
}
@@ -0,0 +1,168 @@
package meshcatalogclient
import (
"encoding/json"
"fmt"
"slices"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The states of a verdict on a manifest (novox/hq ADR 0578 §1): the indexer judges every manifest it takes in once and
// every other part takes the verdict from the seat's state `verdicts`.
const (
// VerdictAccepted is the state of a manifest the judge found no problem in.
VerdictAccepted = "accepted"
// VerdictRefused is the state of a manifest the judge found a problem in; the verdict's problems say each.
VerdictRefused = "refused"
// VerdictManifestWaits is the state of a manifest that carries a field the judge does not know: it is not refused,
// because a newer judge may know the field; the verdict's unknown names each. A reader holds the module as a refused
// one and keeps what runs. In a `modules` value it is written as a refusal naming the fields.
VerdictManifestWaits = "waits"
)
// VerdictsKey is the key of a `verdicts` value: the digest of the manifest it judges.
func VerdictsKey(digest string) (string, error) {
if !keys.IsHex64(digest) {
return "", fmt.Errorf("meshcatalogclient: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return seat.VerdictsByDigestKey(digest), nil
}
// VerdictRefuses says whether v refuses its manifest.
func VerdictRefuses(v seat.VerdictsByDigestValue) bool { return v.State == VerdictRefused }
// VerdictWaits says whether v says the manifest waits for a judge that knows its fields.
func VerdictWaits(v seat.VerdictsByDigestValue) bool { return v.State == VerdictManifestWaits }
// VerdictHeld says whether a reader holds the module as refused: the manifest is refused or waits.
func VerdictHeld(v seat.VerdictsByDigestValue) bool { return VerdictRefuses(v) || VerdictWaits(v) }
// WaitsOn says, in words, what a waiting manifest waits on.
func WaitsOn(v seat.VerdictsByDigestValue) string {
return "it carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet"
}
// TakeVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): held, with its problems, when the indexer
// wrote a refusal, or, for a manifest that waits, with the words that say which fields it waits on. A missing verdict (nil)
// is taken as accepted: every manifest the indexer holds before it has judged it was taken in under the judges that ran
// until then. A reader holds a refused module and names it with VerdictWhy, and reads the rest.
func TakeVerdict(v *seat.VerdictsByDigestValue) (refused bool, problems []string) {
if v == nil || !VerdictHeld(*v) {
return false, nil
}
if VerdictWaits(*v) {
return true, []string{WaitsOn(*v)}
}
return true, v.Problems
}
// VerdictWhy is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
func VerdictWhy(v seat.VerdictsByDigestValue) string {
if !VerdictHeld(v) {
return ""
}
judge := "the indexer's judge"
if v.Judge != "" {
judge += " (" + v.Judge + ")"
}
if VerdictWaits(v) {
return fmt.Sprintf("%s holds the manifest of %s: %s", judge, v.Module, WaitsOn(v))
}
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
for _, p := range v.Problems {
why += " " + p + ";"
}
return why[:len(why)-1]
}
// VerdictAsRefused is v as a reader that does not know the waiting state takes it: a waiting verdict is a refusal naming what
// it waits on, any other verdict is v.
func VerdictAsRefused(v seat.VerdictsByDigestValue) seat.VerdictsByDigestValue {
if !VerdictWaits(v) {
return v
}
v.State, v.Problems, v.Unknown = VerdictRefused, []string{WaitsOn(v)}, nil
return v
}
// SameVerdict says whether v and o say the same of a manifest: the module, the state, the problems, the unknown fields and
// the warnings, whoever judged and when. The indexer writes a verdict only when it is not the same as the one it holds.
func SameVerdict(v, o seat.VerdictsByDigestValue) bool {
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Unknown, o.Unknown) && slices.Equal(v.Warnings, o.Warnings)
}
func checkVerdict(v seat.VerdictsByDigestValue) error {
if err := keys.CheckName("module", v.Module); err != nil {
return err
}
if v.State != VerdictManifestWaits && len(v.Unknown) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s names unknown fields and does not wait", v.Module)
}
switch v.State {
case VerdictAccepted:
if len(v.Problems) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s accepts its manifest and names problems", v.Module)
}
case VerdictRefused:
if len(v.Problems) == 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and names no problem", v.Module)
}
if len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module)
}
case VerdictManifestWaits:
if len(v.Unknown) == 0 || len(v.Problems) > 0 || len(v.Warnings) > 0 {
return fmt.Errorf("meshcatalogclient: the verdict on %s waits and names no unknown field, or names problems or warnings: a wait names the fields", v.Module)
}
default:
return fmt.Errorf("meshcatalogclient: the verdict on %s is %q, none of %s, %s and %s", v.Module, v.State, VerdictAccepted, VerdictRefused, VerdictManifestWaits)
}
if v.Judge == "" {
return fmt.Errorf("meshcatalogclient: the verdict on %s names no judge", v.Module)
}
return nil
}
// EncodeVerdict is v as JSON at the kept values' version; it refuses a verdict without a module or a judge, a state other
// than accepted, refused or waits, a refusal naming no problem and an acceptance naming one.
func EncodeVerdict(v seat.VerdictsByDigestValue) ([]byte, error) {
if err := checkVerdict(v); err != nil {
return nil, err
}
v.V = keys.Version
return json.Marshal(v)
}
// DecodeVerdict reads a `verdicts` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeVerdict
// refuses.
func DecodeVerdict(data []byte) (seat.VerdictsByDigestValue, error) {
if err := keys.CheckVersion(data); err != nil {
return seat.VerdictsByDigestValue{}, err
}
var v seat.VerdictsByDigestValue
if err := json.Unmarshal(data, &v); err != nil {
return seat.VerdictsByDigestValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err)
}
if err := checkVerdict(v); err != nil {
return seat.VerdictsByDigestValue{}, err
}
return v, nil
}
// ModuleVerdict is the verdict a `modules` value carries, or nil when it carries none (taken as accepted, TakeVerdict). A
// refusal naming unknown fields is the wait it was written as.
func ModuleVerdict(module string, m seat.ModulesByModuleValue) *seat.VerdictsByDigestValue {
if m.Verdict == "" && len(m.Warnings) == 0 {
return nil
}
if m.Verdict == "" {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictAccepted, Warnings: m.Warnings}
}
if m.Verdict == VerdictRefused && len(m.Unknown) > 0 {
return &seat.VerdictsByDigestValue{Module: module, State: VerdictManifestWaits, Unknown: m.Unknown}
}
return &seat.VerdictsByDigestValue{Module: module, State: m.Verdict, Problems: m.Problems}
}