From a1ef06b20e9b28ac00ab33c98f3690de37e60b53 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:08:59 +0100 Subject: [PATCH] =?UTF-8?q?ADR=200627=20=C2=A71:=20the=20four=20registry?= =?UTF-8?q?=20seats'=20clients=20publish=20a=20file=20in=20parts=20over=20?= =?UTF-8?q?their=20own=20seat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each client binds mesh-sdk's seat-neutral parts sender to its own seat's publish and publish-part and types, so no client uses another seat's; each is a Go module and a package artifact of its own (ADR 0625 §3). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../containerimageregistryclient/client.go | 63 +++++++++++++++++++ .../client_test.go | 57 +++++++++++++++++ .../containerimageregistryclient/go.mod | 8 +++ go/clients/gopackageregistryclient/client.go | 63 +++++++++++++++++++ .../gopackageregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/gopackageregistryclient/go.mod | 8 +++ go/clients/npmpackageregistryclient/client.go | 63 +++++++++++++++++++ .../npmpackageregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/npmpackageregistryclient/go.mod | 8 +++ .../ociartifactregistryclient/client.go | 63 +++++++++++++++++++ .../ociartifactregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/ociartifactregistryclient/go.mod | 8 +++ module.json | 4 ++ 13 files changed, 516 insertions(+) create mode 100644 go/clients/containerimageregistryclient/client.go create mode 100644 go/clients/containerimageregistryclient/client_test.go create mode 100644 go/clients/containerimageregistryclient/go.mod create mode 100644 go/clients/gopackageregistryclient/client.go create mode 100644 go/clients/gopackageregistryclient/client_test.go create mode 100644 go/clients/gopackageregistryclient/go.mod create mode 100644 go/clients/npmpackageregistryclient/client.go create mode 100644 go/clients/npmpackageregistryclient/client_test.go create mode 100644 go/clients/npmpackageregistryclient/go.mod create mode 100644 go/clients/ociartifactregistryclient/client.go create mode 100644 go/clients/ociartifactregistryclient/client_test.go create mode 100644 go/clients/ociartifactregistryclient/go.mod diff --git a/go/clients/containerimageregistryclient/client.go b/go/clients/containerimageregistryclient/client.go new file mode 100644 index 0000000..4add3df --- /dev/null +++ b/go/clients/containerimageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package containerimageregistryclient is the container-image-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package containerimageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/containerimageregistryclient/client_test.go b/go/clients/containerimageregistryclient/client_test.go new file mode 100644 index 0000000..8ef827a --- /dev/null +++ b/go/clients/containerimageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package containerimageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:container-image-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:container-image-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod new file mode 100644 index 0000000..a58584d --- /dev/null +++ b/go/clients/containerimageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/gopackageregistryclient/client.go b/go/clients/gopackageregistryclient/client.go new file mode 100644 index 0000000..e339065 --- /dev/null +++ b/go/clients/gopackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package gopackageregistryclient is the go-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package gopackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/gopackageregistryclient/client_test.go b/go/clients/gopackageregistryclient/client_test.go new file mode 100644 index 0000000..3180a68 --- /dev/null +++ b/go/clients/gopackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package gopackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:go-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:go-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod new file mode 100644 index 0000000..59bef72 --- /dev/null +++ b/go/clients/gopackageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/npmpackageregistryclient/client.go b/go/clients/npmpackageregistryclient/client.go new file mode 100644 index 0000000..f72a8cf --- /dev/null +++ b/go/clients/npmpackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package npmpackageregistryclient is the npm-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package npmpackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/npmpackageregistryclient/client_test.go b/go/clients/npmpackageregistryclient/client_test.go new file mode 100644 index 0000000..4433025 --- /dev/null +++ b/go/clients/npmpackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package npmpackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:npm-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:npm-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod new file mode 100644 index 0000000..a7815fe --- /dev/null +++ b/go/clients/npmpackageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/ociartifactregistryclient/client.go b/go/clients/ociartifactregistryclient/client.go new file mode 100644 index 0000000..3f9a894 --- /dev/null +++ b/go/clients/ociartifactregistryclient/client.go @@ -0,0 +1,63 @@ +// Package ociartifactregistryclient is the oci-artifact-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package ociartifactregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/ociartifactregistryclient/client_test.go b/go/clients/ociartifactregistryclient/client_test.go new file mode 100644 index 0000000..0f64e6f --- /dev/null +++ b/go/clients/ociartifactregistryclient/client_test.go @@ -0,0 +1,57 @@ +package ociartifactregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:oci-artifact-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:oci-artifact-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod new file mode 100644 index 0000000..a08e021 --- /dev/null +++ b/go/clients/ociartifactregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/module.json b/module.json index 118627c..5fecc68 100644 --- a/module.json +++ b/module.json @@ -5,6 +5,10 @@ "build": { "artifacts": [ { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, + { "name": "go-containerimageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/containerimageregistryclient"], "series": "0.1" }, + { "name": "go-gopackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/gopackageregistryclient"], "series": "0.1" }, + { "name": "go-npmpackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/npmpackageregistryclient"], "series": "0.1" }, + { "name": "go-ociartifactregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/ociartifactregistryclient"], "series": "0.1" }, { "name": "go-containerimageregistry", "kind": "package", "language": "go", "paths": ["go/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-gopackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-meshbroker", "kind": "package", "language": "go", "paths": ["go/seats/meshbroker"], "series": "0.1", "generated-from": "." },