Something went wrong. Try again.
Cloudflare Cap'n'Web Go implementation
Something went wrong. Try again.
capnweb-go expr.go
18 kB · 678 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679package capnweb
import ( "bytes" "encoding/base64" "encoding/json" "fmt" "math/big" "net/http" "time")
// Expr represents a capnweb expression value. All values transmitted over the// protocol are expressions — either literal JSON values or typed arrays like// ["bytes", "..."], ["date", 123], ["import", 1, "method", [args]], etc.//// The array wrapping rule: non-array JSON values are interpreted literally.// Arrays where element 0 is a recognized string tag are typed expressions.// Actual plain arrays must be wrapped: [elem0, elem1] on the wire represents// a two-element array, distinguished from expressions by the first element// not being a known tag string (or by being an ArrayExpr during encoding).type Expr interface { expr() // sealed marker}
// --- Literal expressions ---
// LiteralExpr wraps a plain JSON value (string, number, bool, null, or object).type LiteralExpr struct{ Value any }
func (LiteralExpr) expr() {}
// UndefinedExpr represents JavaScript undefined — ["undefined"].type UndefinedExpr struct{}
func (UndefinedExpr) expr() {}
// InfExpr represents +Infinity — ["inf"].type InfExpr struct{}
func (InfExpr) expr() {}
// NegInfExpr represents -Infinity — ["-inf"].type NegInfExpr struct{}
func (NegInfExpr) expr() {}
// NaNExpr represents NaN — ["nan"].type NaNExpr struct{}
func (NaNExpr) expr() {}
// --- Data type expressions ---
// BytesExpr represents a byte slice — ["bytes", base64].type BytesExpr struct{ Data []byte }
func (BytesExpr) expr() {}
// BigIntExpr represents an arbitrary-precision integer — ["bigint", decimal].type BigIntExpr struct{ Value *big.Int }
func (BigIntExpr) expr() {}
// DateExpr represents a timestamp — ["date", unixMillis].type DateExpr struct{ Time time.Time }
func (DateExpr) expr() {}
// ErrorExpr represents a remote error — ["error", type, message, stack?].type ErrorExpr struct { Type string // "Error", "TypeError", "RangeError", etc. Message string Stack string // optional}
func (ErrorExpr) expr() {}
func (e ErrorExpr) Error() string { return fmt.Sprintf("%s: %s", e.Type, e.Message)}
// HeadersExpr represents HTTP headers — ["headers", [[name, value], ...]].type HeadersExpr struct{ Header http.Header }
func (HeadersExpr) expr() {}
// RequestExpr represents an HTTP request — ["request", url, init].type RequestExpr struct { URL string Method string Headers http.Header Body Expr // may be nil}
func (RequestExpr) expr() {}
// ResponseExpr represents an HTTP response — ["response", body, init].type ResponseExpr struct { Status int StatusText string Headers http.Header Body Expr // may be nil}
func (ResponseExpr) expr() {}
// --- Reference expressions (resolved by the session layer) ---
// ImportExpr references an import table entry — ["import", id, path?, args?].// Evaluates to a stub.type ImportExpr struct { ImportID int64 Path []string Args []Expr // nil = no call; empty = call with zero args}
func (ImportExpr) expr() {}
// PipelineExpr is like ImportExpr but evaluates to a promise —// ["pipeline", id, path?, args?].type PipelineExpr struct { ImportID int64 Path []string Args []Expr}
func (PipelineExpr) expr() {}
// ExportExpr exports a local object — ["export", id].type ExportExpr struct{ ExportID int64 }
func (ExportExpr) expr() {}
// PromiseExpr exports a promise — ["promise", id].type PromiseExpr struct{ ExportID int64 }
func (PromiseExpr) expr() {}
// --- Stream expressions ---
// WritableExpr references a writable stream — ["writable", exportId].type WritableExpr struct{ ExportID int64 }
func (WritableExpr) expr() {}
// ReadableExpr references the readable end of a pipe — ["readable", importId].type ReadableExpr struct{ ImportID int64 }
func (ReadableExpr) expr() {}
// --- Remap expression ---
// RemapExpr represents a server-side .map() — ["remap", importId, path, captures, instructions].type RemapExpr struct { ImportID int64 Path []string Captures []Expr Instructions []Expr}
func (RemapExpr) expr() {}
// --- Array wrapper ---
// ArrayExpr wraps a plain array of expressions. On the wire actual arrays are// encoded as [elem0, elem1, ...] — the encoder handles distinguishing them// from typed expression arrays.type ArrayExpr struct{ Elements []Expr }
func (ArrayExpr) expr() {}
// EncodeExpr serializes an Expr to its JSON wire representation.func EncodeExpr(e Expr) (json.RawMessage, error) { switch v := e.(type) { case LiteralExpr: return json.Marshal(v.Value)
case UndefinedExpr: return json.Marshal([]string{"undefined"}) case InfExpr: return json.Marshal([]string{"inf"}) case NegInfExpr: return json.Marshal([]string{"-inf"}) case NaNExpr: return json.Marshal([]string{"nan"})
case BytesExpr: return json.Marshal([]any{"bytes", base64.StdEncoding.EncodeToString(v.Data)}) case BigIntExpr: return json.Marshal([]any{"bigint", v.Value.String()}) case DateExpr: return json.Marshal([]any{"date", v.Time.UnixMilli()})
case ErrorExpr: if v.Stack != "" { return json.Marshal([]any{"error", v.Type, v.Message, v.Stack}) } return json.Marshal([]any{"error", v.Type, v.Message})
case HeadersExpr: return json.Marshal([]any{"headers", headerPairs(v.Header)})
case RequestExpr: init := map[string]any{} if v.Method != "" { init["method"] = v.Method } if v.Headers != nil { init["headers"] = headerPairs(v.Headers) } if v.Body != nil { body, err := EncodeExpr(v.Body) if err != nil { return nil, fmt.Errorf("capnweb: request body: %w", err) } init["body"] = json.RawMessage(body) } return json.Marshal([]any{"request", v.URL, init})
case ResponseExpr: init := map[string]any{} if v.Status != 0 { init["status"] = v.Status } if v.StatusText != "" { init["statusText"] = v.StatusText } if v.Headers != nil { init["headers"] = headerPairs(v.Headers) } var body json.RawMessage if v.Body != nil { var err error body, err = EncodeExpr(v.Body) if err != nil { return nil, fmt.Errorf("capnweb: response body: %w", err) } } else { body = json.RawMessage("null") } return json.Marshal([]any{"response", json.RawMessage(body), init})
case ImportExpr: return encodeRefExpr("import", v.ImportID, v.Path, v.Args) case PipelineExpr: return encodeRefExpr("pipeline", v.ImportID, v.Path, v.Args)
case ExportExpr: return json.Marshal([]any{"export", v.ExportID}) case PromiseExpr: return json.Marshal([]any{"promise", v.ExportID})
case WritableExpr: return json.Marshal([]any{"writable", v.ExportID}) case ReadableExpr: return json.Marshal([]any{"readable", v.ImportID})
case RemapExpr: caps, err := encodeExprSlice(v.Captures) if err != nil { return nil, fmt.Errorf("capnweb: remap captures: %w", err) } instrs, err := encodeExprSlice(v.Instructions) if err != nil { return nil, fmt.Errorf("capnweb: remap instructions: %w", err) } return json.Marshal([]any{"remap", v.ImportID, v.Path, caps, instrs})
case ArrayExpr: encoded, err := encodeExprSlice(v.Elements) if err != nil { return nil, err } return json.Marshal(encoded)
default: return nil, fmt.Errorf("capnweb: unknown expression type %T", e) }}
// DecodeExpr deserializes a JSON wire value into an Expr.func DecodeExpr(data json.RawMessage) (Expr, error) { // Trim leading whitespace to reliably detect the value type. trimmed := bytes.TrimLeft(data, " \t\n\r") if len(trimmed) == 0 { return nil, fmt.Errorf("capnweb: empty expression") }
// Non-array values are literals. if trimmed[0] != '[' { var v any if err := json.Unmarshal(data, &v); err != nil { return nil, fmt.Errorf("capnweb: invalid literal: %w", err) } return LiteralExpr{Value: v}, nil }
var raw []json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("capnweb: invalid array: %w", err) } if len(raw) == 0 { return ArrayExpr{}, nil }
// If first element isn't a string, it's a plain array. var tag string if err := json.Unmarshal(raw[0], &tag); err != nil { return decodeArrayElements(raw) }
switch tag { case "undefined": return UndefinedExpr{}, nil case "inf": return InfExpr{}, nil case "-inf": return NegInfExpr{}, nil case "nan": return NaNExpr{}, nil
case "bytes": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: bytes: missing data") } var s string if err := json.Unmarshal(raw[1], &s); err != nil { return nil, fmt.Errorf("capnweb: bytes: %w", err) } b, err := base64.StdEncoding.DecodeString(s) if err != nil { return nil, fmt.Errorf("capnweb: bytes base64: %w", err) } return BytesExpr{Data: b}, nil
case "bigint": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: bigint: missing value") } var s string if err := json.Unmarshal(raw[1], &s); err != nil { return nil, fmt.Errorf("capnweb: bigint: %w", err) } n, ok := new(big.Int).SetString(s, 10) if !ok { return nil, fmt.Errorf("capnweb: invalid bigint %q", s) } return BigIntExpr{Value: n}, nil
case "date": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: date: missing value") } var ms float64 if err := json.Unmarshal(raw[1], &ms); err != nil { return nil, fmt.Errorf("capnweb: date: %w", err) } return DateExpr{Time: time.UnixMilli(int64(ms))}, nil
case "error": if len(raw) < 3 { return nil, fmt.Errorf("capnweb: error: need type and message") } var typ, msg string if err := json.Unmarshal(raw[1], &typ); err != nil { return nil, fmt.Errorf("capnweb: error type: %w", err) } if err := json.Unmarshal(raw[2], &msg); err != nil { return nil, fmt.Errorf("capnweb: error message: %w", err) } var stack string if len(raw) > 3 { _ = json.Unmarshal(raw[3], &stack) } return ErrorExpr{Type: typ, Message: msg, Stack: stack}, nil
case "headers": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: headers: missing pairs") } var pairs [][2]string if err := json.Unmarshal(raw[1], &pairs); err != nil { return nil, fmt.Errorf("capnweb: headers: %w", err) } h := make(http.Header, len(pairs)) for _, p := range pairs { h.Add(p[0], p[1]) } return HeadersExpr{Header: h}, nil
case "request": if len(raw) < 3 { return nil, fmt.Errorf("capnweb: request: need url and init") } var url string if err := json.Unmarshal(raw[1], &url); err != nil { return nil, fmt.Errorf("capnweb: request url: %w", err) } var init struct { Method string `json:"method"` Headers [][2]string `json:"headers"` Body json.RawMessage `json:"body"` } if err := json.Unmarshal(raw[2], &init); err != nil { return nil, fmt.Errorf("capnweb: request init: %w", err) } req := RequestExpr{ URL: url, Method: init.Method, } if init.Headers != nil { req.Headers = make(http.Header, len(init.Headers)) for _, p := range init.Headers { req.Headers.Add(p[0], p[1]) } } if len(init.Body) > 0 && string(init.Body) != "null" { body, err := DecodeExpr(init.Body) if err != nil { return nil, fmt.Errorf("capnweb: request body: %w", err) } req.Body = body } return req, nil
case "response": if len(raw) < 3 { return nil, fmt.Errorf("capnweb: response: need body and init") } var body Expr if string(raw[1]) != "null" { var err error body, err = DecodeExpr(raw[1]) if err != nil { return nil, fmt.Errorf("capnweb: response body: %w", err) } } var init struct { Status int `json:"status"` StatusText string `json:"statusText"` Headers [][2]string `json:"headers"` } if err := json.Unmarshal(raw[2], &init); err != nil { return nil, fmt.Errorf("capnweb: response init: %w", err) } resp := ResponseExpr{ Status: init.Status, StatusText: init.StatusText, Body: body, } if init.Headers != nil { resp.Headers = make(http.Header, len(init.Headers)) for _, p := range init.Headers { resp.Headers.Add(p[0], p[1]) } } return resp, nil
case "import": return decodeRefAsImport(raw) case "pipeline": return decodeRefAsPipeline(raw)
case "export": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: export: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: export id: %w", err) } return ExportExpr{ExportID: id}, nil
case "promise": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: promise: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: promise id: %w", err) } return PromiseExpr{ExportID: id}, nil
case "writable": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: writable: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: writable id: %w", err) } return WritableExpr{ExportID: id}, nil
case "readable": if len(raw) < 2 { return nil, fmt.Errorf("capnweb: readable: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: readable id: %w", err) } return ReadableExpr{ImportID: id}, nil
case "remap": if len(raw) < 5 { return nil, fmt.Errorf("capnweb: remap: need 5 elements, got %d", len(raw)) } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: remap id: %w", err) } var path []string if err := json.Unmarshal(raw[2], &path); err != nil { return nil, fmt.Errorf("capnweb: remap path: %w", err) } caps, err := decodeExprSlice(raw[3]) if err != nil { return nil, fmt.Errorf("capnweb: remap captures: %w", err) } instrs, err := decodeExprSlice(raw[4]) if err != nil { return nil, fmt.Errorf("capnweb: remap instructions: %w", err) } return RemapExpr{ImportID: id, Path: path, Captures: caps, Instructions: instrs}, nil
default: // Unknown tag — treat as plain array. return decodeArrayElements(raw) }}
// --- encoding helpers ---
func headerPairs(h http.Header) [][2]string { var pairs [][2]string for name, vals := range h { for _, v := range vals { pairs = append(pairs, [2]string{name, v}) } } return pairs}
func encodeRefExpr(tag string, id int64, path []string, args []Expr) (json.RawMessage, error) { elems := []any{tag, id} if len(path) > 0 || args != nil { if len(path) == 1 { elems = append(elems, path[0]) } else { elems = append(elems, path) } } if args != nil { encoded, err := encodeExprSlice(args) if err != nil { return nil, fmt.Errorf("capnweb: %s args: %w", tag, err) } elems = append(elems, encoded) } return json.Marshal(elems)}
func encodeExprSlice(exprs []Expr) ([]json.RawMessage, error) { out := make([]json.RawMessage, len(exprs)) for i, e := range exprs { b, err := EncodeExpr(e) if err != nil { return nil, err } out[i] = b } return out, nil}
// --- decoding helpers ---
func decodeInt64(data json.RawMessage, dst *int64) error { var n json.Number if err := json.Unmarshal(data, &n); err != nil { return fmt.Errorf("expected number: %w", err) } v, err := n.Int64() if err != nil { return fmt.Errorf("expected integer: %w", err) } *dst = v return nil}
func decodeArrayElements(raw []json.RawMessage) (Expr, error) { elems := make([]Expr, len(raw)) for i, r := range raw { e, err := DecodeExpr(r) if err != nil { return nil, fmt.Errorf("capnweb: array[%d]: %w", i, err) } elems[i] = e } return ArrayExpr{Elements: elems}, nil}
func decodeExprSlice(data json.RawMessage) ([]Expr, error) { var raw []json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return nil, err } out := make([]Expr, len(raw)) for i, r := range raw { e, err := DecodeExpr(r) if err != nil { return nil, fmt.Errorf("[%d]: %w", i, err) } out[i] = e } return out, nil}
func decodeRefPath(raw []json.RawMessage) ([]string, error) { if len(raw) < 3 { return nil, nil } // Path can be a single string or array of strings. var s string if err := json.Unmarshal(raw[2], &s); err == nil { return []string{s}, nil } var path []string if err := json.Unmarshal(raw[2], &path); err != nil { return nil, fmt.Errorf("capnweb: ref path: %w", err) } return path, nil}
func decodeRefArgs(raw []json.RawMessage) ([]Expr, error) { if len(raw) < 4 { return nil, nil } return decodeExprSlice(raw[3])}
func decodeRefAsImport(raw []json.RawMessage) (Expr, error) { if len(raw) < 2 { return nil, fmt.Errorf("capnweb: import: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: import id: %w", err) } path, err := decodeRefPath(raw) if err != nil { return nil, err } args, err := decodeRefArgs(raw) if err != nil { return nil, fmt.Errorf("capnweb: import args: %w", err) } return ImportExpr{ImportID: id, Path: path, Args: args}, nil}
func decodeRefAsPipeline(raw []json.RawMessage) (Expr, error) { if len(raw) < 2 { return nil, fmt.Errorf("capnweb: pipeline: missing id") } var id int64 if err := decodeInt64(raw[1], &id); err != nil { return nil, fmt.Errorf("capnweb: pipeline id: %w", err) } path, err := decodeRefPath(raw) if err != nil { return nil, err } args, err := decodeRefArgs(raw) if err != nil { return nil, fmt.Errorf("capnweb: pipeline args: %w", err) } return PipelineExpr{ImportID: id, Path: path, Args: args}, nil}