A catalogue merge or pull request no longer lists or reads placement/, so the Listed read and ConvertWith have no use and go.
124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"mesh-delivery-forge-bridge/internal/bridge"
|
|
)
|
|
|
|
// callTimeout bounds a call to a tool (mesh-sdk go/process/call.go CallTimeout).
|
|
const callTimeout = 30 * time.Second
|
|
|
|
// caller calls a module's tool by its manifest key (`<module>.<tool>`) with a JSON body and returns the tool's result.
|
|
type caller func(ctx context.Context, key string, body any) (json.RawMessage, error)
|
|
|
|
// callerOn calls tools as the bridge's own principal. It copies mesh-sdk go/process/call.go (Call, callSubjects,
|
|
// request; hand-written here until the SDK's helper is used, issue 1242): the request is the tool's arguments as
|
|
// JSON, published on the caller-named subject `mesh.mod.<module>.call.<tool>.<user, dots as ~>`, or on the subject that
|
|
// names no caller, `mesh.mod.<module>.tool.<tool>`, when nothing answers the first; the reply is
|
|
// {"result", "error", "node"} and a non-empty error is the call's failure (mesh-tools node-tools/internal/bus/bus.go
|
|
// answerOf). The grant to publish on these subjects comes from the manifest's invokes alone (mesh-control
|
|
// internal/broker/nats.go invokedSubjects).
|
|
func callerOn(nc *nats.Conn, user string) caller {
|
|
token := strings.ReplaceAll(user, ".", "~")
|
|
request := func(ctx context.Context, subject string, data []byte) (json.RawMessage, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, callTimeout)
|
|
defer cancel()
|
|
msg, err := nc.RequestWithContext(ctx, subject, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var r struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error string `json:"error"`
|
|
Node string `json:"node"`
|
|
}
|
|
if err := json.Unmarshal(msg.Data, &r); err != nil {
|
|
return nil, fmt.Errorf("%s answered something that is not a tool's answer: %w", subject, err)
|
|
}
|
|
if r.Error != "" {
|
|
return nil, errors.New(r.Error)
|
|
}
|
|
return r.Result, nil
|
|
}
|
|
return func(ctx context.Context, key string, body any) (json.RawMessage, error) {
|
|
module, tool, ok := strings.Cut(key, ".")
|
|
if !ok || module == "" || tool == "" {
|
|
return nil, fmt.Errorf("%q is not <module>.<tool>", key)
|
|
}
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := request(ctx, "mesh.mod."+module+".call."+tool+"."+token, data)
|
|
if errors.Is(err, nats.ErrNoResponders) {
|
|
result, err = request(ctx, "mesh.mod."+module+".tool."+tool, data)
|
|
}
|
|
return result, err
|
|
}
|
|
}
|
|
|
|
// commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea, issue 1234): arguments owner,
|
|
// repo and sha, answer {root, trees}, the root directory spelled "" (1234/AC4).
|
|
const commitTreesTool = "gitea.gitea_commit_trees"
|
|
|
|
// treesVia asks the forge for a commit's trees through call.
|
|
func treesVia(ctx context.Context, call caller) bridge.Trees {
|
|
return func(owner, repo, sha string) (string, map[string]string, error) {
|
|
raw, err := call(ctx, commitTreesTool, map[string]string{"owner": owner, "repo": repo, "sha": sha})
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
var a struct {
|
|
Root string `json:"root"`
|
|
Trees map[string]string `json:"trees"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return "", nil, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err)
|
|
}
|
|
if a.Root == "" {
|
|
return "", nil, fmt.Errorf("the answer of %s names no root tree", commitTreesTool)
|
|
}
|
|
return a.Root, a.Trees, nil
|
|
}
|
|
}
|
|
|
|
// getFileTool is the forge's tool for one file at a ref (mesh-catalog modules/gitea tools/index.ts gitea_get_file):
|
|
// arguments owner, repo, path and ref, answer {file: {path, ref, sha, size, content}}, the content decoded to text
|
|
// (client.ts getFile). A path the forge does not have fails with `Gitea API <path>: 404 <body>` (client.ts request).
|
|
const getFileTool = "gitea.gitea_get_file"
|
|
|
|
// notFound is how the forge's error says 404 (client.ts request: `Gitea API ${path}: ${status} ${text}`; client.ts:670
|
|
// recognises it as /: 404 /).
|
|
var notFound = regexp.MustCompile(`: 404 `)
|
|
|
|
// filesVia asks the forge for a file through call. A file the forge says is not there is bridge.ErrNotFound.
|
|
func filesVia(ctx context.Context, call caller) bridge.Files {
|
|
return func(owner, repo, path, ref string) ([]byte, error) {
|
|
raw, err := call(ctx, getFileTool, map[string]string{"owner": owner, "repo": repo, "path": path, "ref": ref})
|
|
if err != nil {
|
|
if notFound.MatchString(err.Error()) {
|
|
return nil, fmt.Errorf("%w: %v", bridge.ErrNotFound, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
var a struct {
|
|
File *struct {
|
|
Content *string `json:"content"`
|
|
} `json:"file"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil || a.File == nil || a.File.Content == nil {
|
|
return nil, fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err)
|
|
}
|
|
return []byte(*a.File.Content), nil
|
|
}
|
|
}
|