Files
mesh-delivery/modules/mesh-delivery-forge-bridge/internal/bridge/call.go
T
jschoubben 71a35a6376 MV1: move contracts and sdk under modules/, importers follow
The delivery repository's contracts and SDK live under modules/ as
mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths
renamed to match the folders (the TypeScript names already did), and
every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools
and mesh-sdk follows in the same group: a design as it should be, one
path per module, checked by the group's check building every importer.

Builds: design 47 G §427, §88

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:09:56 +01:00

109 lines
4.6 KiB
Go

package bridge
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
git "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/git"
)
// Caller calls a seat's verb by its manifest key (`seat:<seat>.<verb>`) with a JSON body and returns its answer: the
// bridge's process calls through the bus as its own principal, its tools bundle through the runtime (stdio.Ask).
type Caller func(ctx context.Context, key string, body any) (json.RawMessage, error)
// GitKey is the manifest key of a verb of the seat `git` (novox/hq design 47 *The seat `git`*): the bridge reaches the
// forge through the seat alone and names no module that holds it.
func GitKey(verb string) string { return "seat:" + git.Seat + "." + verb }
// askGit calls one verb of the seat `git` and decodes its answer into A. An answer the contract cannot type, or one
// that says an error, is the call's error: a failed read is never read as none (design 47 *A failed read answers an
// error*).
func askGit[A any](ctx context.Context, call Caller, verb string, in any) (A, error) {
var a A
raw, err := call(ctx, GitKey(verb), in)
if err != nil {
return a, fmt.Errorf("the seat git did not answer %s: %w", verb, err)
}
if err := json.Unmarshal(raw, &a); err != nil {
return a, fmt.Errorf("the seat git's answer of %s is not its contract's: %w", verb, err)
}
var said struct {
Error string `json:"error"`
}
_ = json.Unmarshal(raw, &said)
if said.Error != "" {
return a, fmt.Errorf("the seat git refused %s: %s", verb, said.Error)
}
return a, nil
}
// repository is `<owner>/<name>`, as the seat names a repository.
func repository(owner, repo string) string { return owner + "/" + repo }
// TreesVia asks the seat `git` for a commit's trees (verb trees).
func TreesVia(ctx context.Context, call Caller) Trees {
return func(owner, repo, sha string) (string, map[string]string, error) {
a, err := askGit[git.TreesAnswer](ctx, call, git.VerbTrees, git.TreesInput{Repository: repository(owner, repo), Commit: sha})
if err != nil {
return "", nil, err
}
if a.Root == "" {
return "", nil, fmt.Errorf("the seat git's trees of %s at %s name no root tree", repository(owner, repo), sha)
}
return a.Root, a.Trees, nil
}
}
// FilesVia asks the seat `git` for one file at a ref (verb file). A file the seat says is absent is ErrNotFound; an
// answer that says neither absent nor content is an error, never an empty file.
func FilesVia(ctx context.Context, call Caller) Files {
return func(owner, repo, path, ref string) ([]byte, error) {
raw, err := call(ctx, GitKey(git.VerbFile), git.FileInput{Repository: repository(owner, repo), Path: path, Ref: ref})
if err != nil {
return nil, fmt.Errorf("the seat git did not answer file: %w", err)
}
var a struct {
git.FileAnswer
Content *string `json:"content"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the seat git's answer of file is not its contract's: %w", err)
}
switch {
case a.Error != "":
return nil, fmt.Errorf("the seat git refused file %s at %s: %s", path, ref, a.Error)
case a.Absent:
return nil, fmt.Errorf("%w: %s at %s in %s", ErrNotFound, path, ref, repository(owner, repo))
case a.Content == nil:
return nil, fmt.Errorf("the seat git's file %s at %s says neither its content nor that it is absent", path, ref)
}
return []byte(*a.Content), nil
}
}
// errNoAnswer is a read whose answer carries nothing the contract says it must.
var errNoAnswer = errors.New("the seat git's answer carries nothing")
// MergedMessage is the merged message of a merge into the trunk, asked again: built as the bridge builds the one it writes
// on FORGE (it is that event's body through ConvertKept), from the pull request's owner, repository, number, branch, head
// and merge commit, which the seat git's verb pull answers. It reads the forge for every module directory (no kept inputs).
func MergedMessage(owner, repo string, number int, branch, head, merge string, trees Trees, files Files) (Message, error) {
body, err := json.Marshal(git.PullMergedEvent{Repository: repository(owner, repo), Number: int64(number), Branch: branch, Head: head, Base: Trunk, MergeCommit: merge})
if err != nil {
return Message{}, err
}
return ConvertKept(MergedSubject, body, trees, files, nil)
}
// splitRepository is a `<owner>/<name>` cut in two, refusing anything else.
func splitRepository(repository string) (string, string, error) {
owner, name, ok := strings.Cut(strings.TrimSpace(repository), "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", "", fmt.Errorf("repository %q is not <owner>/<name>", repository)
}
return owner, name, nil
}