CT13 (reserved's answers and its client): reserved answers released or error, and meshbuildsclient asks it
reserved answers module, version, artifacts and released (a boolean), or error alone; module, version and artifacts are read as required only without error. contracts/go/clients/meshbuildsclient (artifact go-meshbuildsclient) has Reserved, which answers the reservation or release, a Refusal for the error and an error for an answer that is not one. Builds: design 47 G §679, §582, §586, §650 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// Package meshbuildsclient is the mesh-builds seat's client (novox/hq ADR 0573 §1, ADR 0627 §1; design 47 G §586): the
|
||||
// registry holders' call of `reserved`, its input and each of its answers, written once. The call is the caller's own (a
|
||||
// bundle's stdio.Ask, a process's call of a seat verb), so the client holds no transport.
|
||||
package meshbuildsclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
)
|
||||
|
||||
// Address is the seat verb asked, as a manifest's `invokes` names it.
|
||||
const Address = "seat:" + seat.Seat + "." + seat.VerbReserved
|
||||
|
||||
// Call asks one verb of the seat, `seat:<seat>.<verb>`, with a body, and answers its JSON.
|
||||
type Call func(address string, body any) (json.RawMessage, error)
|
||||
|
||||
// Refusal is the error answer of `reserved`: a name no entry carries, or a version neither reserved nor released, in the
|
||||
// builder's words.
|
||||
type Refusal struct{ Words string }
|
||||
|
||||
func (r Refusal) Error() string { return Address + ": " + r.Words }
|
||||
|
||||
// Reservation is a reserved or a released version with each artifact and its digest (design 47 G §679).
|
||||
type Reservation struct {
|
||||
Module string
|
||||
Version string
|
||||
Artifacts []seat.ReservedAnswerArtifactsItem
|
||||
// Released is true when the catalog holds the version released and Artifacts are the digests its release recorded.
|
||||
Released bool
|
||||
}
|
||||
|
||||
// Reserved asks the seat for a module's, or a package's, reserved version. It answers the reservation or release, a
|
||||
// Refusal for the answer `error`, and an error for an answer that cannot be read or lacks a module or a version without
|
||||
// saying error: an answer that cannot be read is never a yes.
|
||||
func Reserved(call Call, in seat.ReservedInput) (Reservation, error) {
|
||||
raw, err := call(Address, in)
|
||||
if err != nil {
|
||||
return Reservation{}, fmt.Errorf("%s did not answer: %w", Address, err)
|
||||
}
|
||||
var a seat.ReservedAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return Reservation{}, fmt.Errorf("%s answered what cannot be read: %s", Address, raw)
|
||||
}
|
||||
if a.Error != "" {
|
||||
return Reservation{}, Refusal{Words: a.Error}
|
||||
}
|
||||
if a.Module == "" || a.Version == "" {
|
||||
return Reservation{}, fmt.Errorf("%s answered no reservation: %s", Address, raw)
|
||||
}
|
||||
return Reservation{Module: a.Module, Version: a.Version, Artifacts: a.Artifacts, Released: a.Released}, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package meshbuildsclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
||||
)
|
||||
|
||||
func answers(raw string, err error) Call {
|
||||
return func(address string, body any) (json.RawMessage, error) {
|
||||
if address != "seat:mesh-builds.reserved" {
|
||||
return nil, errors.New("asked " + address)
|
||||
}
|
||||
return json.RawMessage(raw), err
|
||||
}
|
||||
}
|
||||
|
||||
// TestReservedReadsEachAnswer is design 47 G §586's check: the client's input and each answer read (§679's three).
|
||||
func TestReservedReadsEachAnswer(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
call Call
|
||||
want Reservation
|
||||
refusal string
|
||||
failedOn string
|
||||
}{
|
||||
{name: "a reservation", call: answers(`{"module":"m","version":"0.1.2","artifacts":[{"artifact":"m:go-m","digest":"sha256:aa"}]}`, nil),
|
||||
want: Reservation{Module: "m", Version: "0.1.2", Artifacts: []seat.ReservedAnswerArtifactsItem{{Artifact: "m:go-m", Digest: "sha256:aa"}}}},
|
||||
{name: "a release", call: answers(`{"module":"m","version":"0.1.1","artifacts":[{"artifact":"m:go-m","digest":"sha256:bb"}],"released":true}`, nil),
|
||||
want: Reservation{Module: "m", Version: "0.1.1", Artifacts: []seat.ReservedAnswerArtifactsItem{{Artifact: "m:go-m", Digest: "sha256:bb"}}, Released: true}},
|
||||
{name: "an error alone", call: answers(`{"error":"no entry carries the name x"}`, nil), refusal: "no entry carries the name x"},
|
||||
{name: "no module and no error", call: answers(`{}`, nil), failedOn: "answered no reservation"},
|
||||
{name: "a call that fails", call: answers("", errors.New("no responders")), failedOn: "did not answer"},
|
||||
{name: "what cannot be read", call: answers(`[1]`, nil), failedOn: "cannot be read"},
|
||||
} {
|
||||
got, err := Reserved(tc.call, seat.ReservedInput{Name: "x", Version: "0.1.1"})
|
||||
var r Refusal
|
||||
switch {
|
||||
case tc.refusal != "":
|
||||
if !errors.As(err, &r) || r.Words != tc.refusal {
|
||||
t.Errorf("%s: %v, want a refusal %q", tc.name, err, tc.refusal)
|
||||
}
|
||||
case tc.failedOn != "":
|
||||
if err == nil || !strings.Contains(err.Error(), tc.failedOn) {
|
||||
t.Errorf("%s: %v, want an error saying %q", tc.name, err, tc.failedOn)
|
||||
}
|
||||
default:
|
||||
if err != nil || got.Module != tc.want.Module || got.Version != tc.want.Version || got.Released != tc.want.Released ||
|
||||
len(got.Artifacts) != 1 || got.Artifacts[0] != tc.want.Artifacts[0] {
|
||||
t.Errorf("%s: %+v %v, want %+v", tc.name, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshbuildsclient
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000
|
||||
@@ -117,11 +117,15 @@ type ReservedAnswerArtifactsItem struct {
|
||||
|
||||
// ReservedAnswer is what reserved answers.
|
||||
type ReservedAnswer struct {
|
||||
Artifacts []ReservedAnswerArtifactsItem `json:"artifacts"`
|
||||
Artifacts []ReservedAnswerArtifactsItem `json:"artifacts,omitempty"`
|
||||
// a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679)
|
||||
Error string `json:"error,omitempty"`
|
||||
// the module
|
||||
Module string `json:"module"`
|
||||
Module string `json:"module,omitempty"`
|
||||
// true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679)
|
||||
Released bool `json:"released,omitempty"`
|
||||
// the reserved version, MAJOR.MINOR.PATCH
|
||||
Version string `json:"version"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// PackagePublishedEvent is the body of the event package-published.
|
||||
|
||||
@@ -27,6 +27,10 @@ func TestFixtures(t *testing.T) {
|
||||
decode: func(b []byte) (any, error) { var v ReservedInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "reserved answers the version and each artifact's digest", subject: "", given: "{\"artifacts\":[{\"artifact\":\"mesh-sdk:go\",\"digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},{\"artifact\":\"mesh-sdk:ts\"}],\"module\":\"mesh-sdk\",\"version\":\"0.1.15\"}", wire: "{\"artifacts\":[{\"artifact\":\"mesh-sdk:go\",\"digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},{\"artifact\":\"mesh-sdk:ts\"}],\"module\":\"mesh-sdk\",\"version\":\"0.1.15\"}",
|
||||
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "reserved answers a released version with the digests the release recorded", subject: "", given: "{\"artifacts\":[{\"artifact\":\"mesh-tools:go-meshtools\",\"digest\":\"sha256:aa\"}],\"module\":\"mesh-tools\",\"released\":true,\"version\":\"0.1.4\"}", wire: "{\"artifacts\":[{\"artifact\":\"mesh-tools:go-meshtools\",\"digest\":\"sha256:aa\"}],\"module\":\"mesh-tools\",\"released\":true,\"version\":\"0.1.4\"}",
|
||||
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "reserved answers an error alone, naming the name or the version", subject: "", given: "{\"error\":\"no entry carries the name example.org/none\"}", wire: "{\"error\":\"no entry carries the name example.org/none\"}",
|
||||
decode: func(b []byte) (any, error) { var v ReservedAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v, err := c.decode([]byte(c.given))
|
||||
|
||||
@@ -315,6 +315,15 @@
|
||||
],
|
||||
"series": "0.1"
|
||||
},
|
||||
{
|
||||
"name": "go-meshbuildsclient",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"go/clients/meshbuildsclient"
|
||||
],
|
||||
"series": "0.1"
|
||||
},
|
||||
{
|
||||
"name": "go-meshbuilds",
|
||||
"kind": "package",
|
||||
|
||||
@@ -30,5 +30,27 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reserved answers a released version with the digests the release recorded",
|
||||
"of": "verb:reserved.answer",
|
||||
"given": {
|
||||
"module": "mesh-tools",
|
||||
"version": "0.1.4",
|
||||
"artifacts": [
|
||||
{
|
||||
"artifact": "mesh-tools:go-meshtools",
|
||||
"digest": "sha256:aa"
|
||||
}
|
||||
],
|
||||
"released": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "reserved answers an error alone, naming the name or the version",
|
||||
"of": "verb:reserved.answer",
|
||||
"given": {
|
||||
"error": "no entry carries the name example.org/none"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -64,13 +64,17 @@
|
||||
"artifact"
|
||||
]
|
||||
}
|
||||
},
|
||||
"released": {
|
||||
"type": "boolean",
|
||||
"description": "true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679)"
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"module",
|
||||
"version",
|
||||
"artifacts"
|
||||
]
|
||||
"description": "the reservation or release, or the error; module, version and artifacts are present whenever error is not (design 47 G 679)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
+7
-3
@@ -93,11 +93,15 @@ export interface ReservedAnswerArtifactsItem {
|
||||
|
||||
/** is what reserved answers. */
|
||||
export interface ReservedAnswer {
|
||||
"artifacts": ReservedAnswerArtifactsItem[];
|
||||
"artifacts"?: ReservedAnswerArtifactsItem[];
|
||||
/** a name no entry carries, or a version neither reserved nor released, naming which; set alone (design 47 G 582, 679) */
|
||||
"error"?: string;
|
||||
/** the module */
|
||||
"module": string;
|
||||
"module"?: string;
|
||||
/** true when the version is released: the catalog holds it, and artifacts are the ones its release recorded with their digests; false when it is reserved (design 47 G 582, 679) */
|
||||
"released"?: boolean;
|
||||
/** the reserved version, MAJOR.MINOR.PATCH */
|
||||
"version": string;
|
||||
"version"?: string;
|
||||
}
|
||||
|
||||
/** is the body of the event package-published. */
|
||||
|
||||
Reference in New Issue
Block a user