mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
A typed op could only take one JSON value and answer one, so uploads, signed webhooks, form posts, YAML and merge-patch bodies, PDFs, SSE, WebSockets, OAuth redirects and upstream relays stayed untyped routes with no schema, tool, command or SDK method. Each is now a typed op and its REST wire is unchanged. Requests: a zip.Body field takes the body and Content-Type exactly as sent; an In implementing Parser decodes itself; form: fields and *zip.File parts bind url-encoded and multipart forms; cookie: binds a request cookie as an in: cookie parameter. Consumes names the media. Answers: zip.Body (bytes, filename, its own status and headers, a Reader streams chunked), Verbatim[T] (a relay documented as T), Sse[T] (events with keep-alive), Socket[M] (an upgrade after the contract runs; 426 without one, 501 over ZAP), Redirect, CookieCoder, Produces, and unions: a type whose OneOf method's results are the alternatives, with Or[A,B] as the stock one filing each under the status it states. Projections follow from the manifest: the document publishes binary media, form schemas, x-events, x-socket with 101, 3xx with Location and oneOf; MCP takes bytes as base64 and returns bytes, text or collected events as tool content, listing a socket op in _meta as no tool; the CLI reads --body @file|-, --field name=@file and --out, prints events as they arrive and bridges a socket to stdin and stdout. tools/list is ProjectMCP over the manifest, the projection MCPTools already was. Prose reaches promoted fields (filed under the declaring struct), a type defined over another struct, and the type arguments of zip's kinds and OneOf results; an instantiated generic is published under its name followed by its argument names instead of an illegal component key. The unused reflect-only document and tool builders go.
189 lines
4.9 KiB
Go
189 lines
4.9 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zip
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
)
|
|
|
|
// A union is a type with a OneOf method. The method's RESULT TYPES are the
|
|
// alternatives, and nothing calls it: the signature is the declaration.
|
|
//
|
|
// // Ingest is one event, a list of them, or a batch envelope.
|
|
// type Ingest struct{ … }
|
|
// func (Ingest) OneOf() (Event, []Event, Batch) { return Event{}, nil, Batch{} }
|
|
//
|
|
// The document publishes oneOf over the alternatives, for a request body and
|
|
// for an answer alike, each alternative described exactly as it would be alone.
|
|
// The type decodes and encodes itself (UnmarshalJSON, MarshalJSON, or [Parser]
|
|
// for a request), because which alternative a body is belongs to the type.
|
|
//
|
|
// An answer's alternative may state its own status with a value-receiver
|
|
// StatusCode, which is read off its zero value: the document files the
|
|
// alternative under that status, and the op declares it. [Or] is the union
|
|
// zip writes for itself, and the one whose alternatives may be streams.
|
|
|
|
// alternatives are a union type's alternatives, or nil.
|
|
func alternatives(t reflect.Type) []reflect.Type {
|
|
for t != nil && t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
m, ok := t.MethodByName("OneOf")
|
|
if !ok {
|
|
m, ok = reflect.PointerTo(t).MethodByName("OneOf")
|
|
}
|
|
if !ok || m.Type.NumIn() != 1 || m.Type.NumOut() < 2 {
|
|
return nil
|
|
}
|
|
out := make([]reflect.Type, m.Type.NumOut())
|
|
for i := range out {
|
|
out[i] = m.Type.Out(i)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Or is one of two answers, or one of two request bodies: exactly one of A and
|
|
// B is set. Or[Out, Sse[T]] is an op that answers one JSON value or an event
|
|
// stream; Or[Created, Existing] files each under the status it states.
|
|
//
|
|
// As a request body it decodes into the alternative the JSON is: an
|
|
// alternative whose kind (object, array, string, number, boolean) matches is
|
|
// tried first, refusing unknown fields, A before B; then each is tried as
|
|
// encoding/json would read it.
|
|
type Or[A, B any] struct {
|
|
// A is the first alternative, when it is the one this value holds.
|
|
A *A
|
|
// B is the second alternative, when it is the one this value holds.
|
|
B *B
|
|
}
|
|
|
|
// OneOf declares the alternatives.
|
|
func (Or[A, B]) OneOf() (A, B) {
|
|
var a A
|
|
var b B
|
|
return a, b
|
|
}
|
|
|
|
// isOr reports whether t is an [Or], whose alternatives the seam unwraps at
|
|
// run time; any other union is a value that writes itself.
|
|
func isOr(t reflect.Type) bool {
|
|
_, ok := reflect.New(t).Interface().(interface{ either() any })
|
|
return ok
|
|
}
|
|
|
|
// either is the alternative the value holds, nil when it holds none.
|
|
func (o Or[A, B]) either() any {
|
|
switch {
|
|
case o.A != nil:
|
|
return o.A
|
|
case o.B != nil:
|
|
return o.B
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON writes the alternative the value holds, null when it holds none.
|
|
func (o Or[A, B]) MarshalJSON() ([]byte, error) {
|
|
v := o.either()
|
|
if v == nil {
|
|
return []byte("null"), nil
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
// UnmarshalJSON reads whichever alternative the JSON is.
|
|
func (o *Or[A, B]) UnmarshalJSON(data []byte) error {
|
|
o.A, o.B = nil, nil
|
|
if string(bytes.TrimSpace(data)) == "null" {
|
|
return nil
|
|
}
|
|
var a A
|
|
var b B
|
|
kind := jsonKind(data)
|
|
if kindJSON(reflect.TypeOf(&a).Elem()) == kind && strict(data, &a) == nil {
|
|
o.A = &a
|
|
return nil
|
|
}
|
|
if kindJSON(reflect.TypeOf(&b).Elem()) == kind && strict(data, &b) == nil {
|
|
o.B = &b
|
|
return nil
|
|
}
|
|
a, b = *new(A), *new(B)
|
|
errA := json.Unmarshal(data, &a)
|
|
if errA == nil {
|
|
o.A = &a
|
|
return nil
|
|
}
|
|
if json.Unmarshal(data, &b) == nil {
|
|
o.B = &b
|
|
return nil
|
|
}
|
|
return fmt.Errorf("the value is neither alternative: %w", errA)
|
|
}
|
|
|
|
// strict decodes refusing a field the type does not have.
|
|
func strict(data []byte, v any) error {
|
|
d := json.NewDecoder(bytes.NewReader(data))
|
|
d.DisallowUnknownFields()
|
|
return d.Decode(v)
|
|
}
|
|
|
|
// jsonKind is the kind of a JSON value by its first byte.
|
|
func jsonKind(data []byte) string {
|
|
data = bytes.TrimSpace(data)
|
|
if len(data) == 0 {
|
|
return ""
|
|
}
|
|
switch data[0] {
|
|
case '{':
|
|
return "object"
|
|
case '[':
|
|
return "array"
|
|
case '"':
|
|
return "string"
|
|
case 't', 'f':
|
|
return "boolean"
|
|
}
|
|
return "number"
|
|
}
|
|
|
|
// kindJSON is the kind of JSON value a type reads.
|
|
func kindJSON(t reflect.Type) string {
|
|
for t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if isMarshaler(t) || readsText(t) {
|
|
switch schemaOf(t, nil, nil)["type"] {
|
|
case "integer", "number":
|
|
return "number"
|
|
case "boolean", "object", "array":
|
|
return schemaOf(t, nil, nil)["type"].(string)
|
|
}
|
|
return "string"
|
|
}
|
|
switch t.Kind() {
|
|
case reflect.Struct, reflect.Map:
|
|
return "object"
|
|
case reflect.Slice, reflect.Array:
|
|
if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
|
|
return "string"
|
|
}
|
|
return "array"
|
|
case reflect.String:
|
|
return "string"
|
|
case reflect.Bool:
|
|
return "boolean"
|
|
case reflect.Interface:
|
|
return ""
|
|
}
|
|
return "number"
|
|
}
|