mirror of
https://github.com/hanzoai/zip.git
synced 2026-10-30 20:11:40 +00:00
* wipe upstream zeekay/zip — rebuilding on Fiber v3
* core: zip on Fiber v3 — App, Ctx, Router, typed handlers, OpenAPI
zip wraps *fiber.App. Sinatra/Express idiom (app.Get/Post/...).
Generic zip.Get[I,O], zip.Post[I,O] etc. reflection-build OpenAPI 3.1
spec served at /.well-known/openapi.json + Swagger UI at /docs.
Ctx exposes Hanzo identity (Org/User/UserEmail/IsAdmin) from gateway
X-* headers per HIP-0026. luxfi/log per-request logger via c.Log().
Module(method+path, runtime, modulePath) mounts a HIP-0105 extension
as a Fiber route. Loader is duck-typed via zip/runtime.Loader so zip
does not pull hanzoai/base as a transitive dep — service binary
constructs *extruntime.Loader and injects via zip.Config.Loader.
go.mod: go 1.26.3 (forced by luxfi/log v1.4.3 floor; brief said
1.23.6 but luxfi canonical requires 1.26.x). Fiber v3.2.0, no major
bumps planned.
* middleware: Recover, RequestID, Logger, Timeout, MaxBody, CORS, Auth, RateLimit, Telemetry
Recover catches handler panics → luxfi/log + 500 JSON.
RequestID forwards X-Request-Id (or mints 16-byte hex).
Logger wraps each request with method/path/status/dur, enriches the
per-request luxfi/log Logger via c.SetLog().
Auth trusts gateway-minted X-* headers first; falls back to bearer
verification via injected AuthVerifier.
StripIdentityHeaders for non-gateway deployments.
RateLimit is per-org (or per-IP fallback) in-memory token bucket.
Telemetry plumbs to any O11ySink.
* adapters + wsx + zaprpc
AdaptNetHTTP/AdaptNetHTTPFunc/AdaptNetHTTPMiddleware via Fiber's
adaptor pkg. app.Mount(prefix, http.Handler) for legacy chi/gin/beego
migration. Each adapter doc'd as MIGRATION TOOL — costs ~5% vs native
Fiber dispatch.
wsx is fasthttp/websocket wrapped — wsx.Upgrade(fn) returns a
zip.Handler. Conn is *websocket.Conn from fasthttp/websocket; text/
binary/close/ping/pong message constants re-exported.
zaprpc ships Service + Registry + Dispatch. app.ZAPRegistry() returns
the per-App registry. app.ZAPListen(addr) reserves the port and logs
but the on-the-wire dispatcher is stubbed — full integration in a
follow-up PR pending zapc server type stabilization.
* examples + core tests
examples/
hello/ — minimal Sinatra-style API
zap-typed/ — generic typed handler + auto-OpenAPI + Swagger
subsystem-mount/ — HIP-0106 Mount(*App, deps) idiom
module-routes/ — app.Module() over a stub zip/runtime.Loader
websocket/ — wsx.Upgrade echo server
sse-streaming/ — c.SendStreamWriter Server-Sent Events
migrate-from-gin/ — gin→zip mechanical port
migrate-from-chi/ — chi→zip via AdaptNetHTTP
migrate-from-beego/— beego→zip via AdaptNetHTTP
Verified live: hello binary serves /hello + /health 200s; zap-typed
serves /v1/validate + /.well-known/openapi.json with reflection-
derived schemas. Tests pass: 5 cases (basic routing, HTTPError, typed
validation, groups, identity headers).
* docs: README, HANZO_CHANGES, MIGRATION, MIDDLEWARE, MODULE_HANDLERS
README pitches zip as canonical Hanzo Go framework on Fiber v3.
HANZO_CHANGES documents the diff from upstream zeekay/zip — every
file replaced; only the name kept.
MIGRATION covers gin/chi/beego/net-http with per-route recipes.
MIDDLEWARE covers Recover/RequestID/Logger/Auth/etc.
MODULE_HANDLERS covers app.Module() over HIP-0105 with the wire
envelope and per-runtime guidance.
* ctx.Bind validates via reflection tags; tighten AuthVerifier interface
c.Bind/BindQuery/BindURI now run validate() after parse and return
*HTTPError(400) directly — handler code is one line:
if err := c.Bind(&v); err != nil { return err }
AuthVerifier.Verify now takes context.Context (was an empty
interface{}). middleware import adjusted.
* module-routes: default Content-Type to application/json; add tests
Modules that don't set a Content-Type in their response envelope get
application/json; charset=utf-8 by default — the wire format is JSON
both directions.
Tests cover: happy path (POST /v1/eval echoed back), runtime mismatch
between caller and manifest, AllowedRuntimes refusal, nil Loader.
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package zip
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// validate runs minimal struct-tag validation: required, min=N, max=N,
|
|
// minlen=N, maxlen=N. Operates on string / numeric / slice fields.
|
|
// Anything else passes through.
|
|
//
|
|
// This is deliberately not a full validator (no `github.com/go-playground/validator`
|
|
// — we don't want the dependency). It covers ~90% of REST API input
|
|
// validation needs and is ~120 LOC.
|
|
func validate(v any) error {
|
|
val := reflect.ValueOf(v)
|
|
if val.Kind() == reflect.Pointer {
|
|
if val.IsNil() {
|
|
return nil
|
|
}
|
|
val = val.Elem()
|
|
}
|
|
if val.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
t := val.Type()
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
tag := field.Tag.Get("validate")
|
|
if tag == "" || tag == "-" {
|
|
continue
|
|
}
|
|
if err := validateField(field.Name, val.Field(i), tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateField(name string, val reflect.Value, tag string) error {
|
|
rules := strings.Split(tag, ",")
|
|
for _, r := range rules {
|
|
r = strings.TrimSpace(r)
|
|
if r == "" {
|
|
continue
|
|
}
|
|
var key, raw string
|
|
if i := strings.IndexByte(r, '='); i >= 0 {
|
|
key, raw = r[:i], r[i+1:]
|
|
} else {
|
|
key = r
|
|
}
|
|
switch key {
|
|
case "required":
|
|
if isZero(val) {
|
|
return fmt.Errorf("field %q is required", name)
|
|
}
|
|
case "min":
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("field %q invalid min=%q", name, raw)
|
|
}
|
|
if !checkMin(val, n) {
|
|
return fmt.Errorf("field %q must be >= %d", name, n)
|
|
}
|
|
case "max":
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("field %q invalid max=%q", name, raw)
|
|
}
|
|
if !checkMax(val, n) {
|
|
return fmt.Errorf("field %q must be <= %d", name, n)
|
|
}
|
|
case "minlen":
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("field %q invalid minlen=%q", name, raw)
|
|
}
|
|
if !checkLenMin(val, n) {
|
|
return fmt.Errorf("field %q length must be >= %d", name, n)
|
|
}
|
|
case "maxlen":
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("field %q invalid maxlen=%q", name, raw)
|
|
}
|
|
if !checkLenMax(val, n) {
|
|
return fmt.Errorf("field %q length must be <= %d", name, n)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isZero(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
return v.Len() == 0
|
|
case reflect.Slice, reflect.Map, reflect.Array:
|
|
return v.Len() == 0
|
|
case reflect.Pointer, reflect.Interface:
|
|
return v.IsNil()
|
|
}
|
|
return v.IsZero()
|
|
}
|
|
|
|
func checkMin(v reflect.Value, n int) bool {
|
|
switch v.Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int() >= int64(n)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return v.Uint() >= uint64(n)
|
|
case reflect.Float32, reflect.Float64:
|
|
return v.Float() >= float64(n)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkMax(v reflect.Value, n int) bool {
|
|
switch v.Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int() <= int64(n)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return v.Uint() <= uint64(n)
|
|
case reflect.Float32, reflect.Float64:
|
|
return v.Float() <= float64(n)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkLenMin(v reflect.Value, n int) bool {
|
|
switch v.Kind() {
|
|
case reflect.String, reflect.Slice, reflect.Map, reflect.Array:
|
|
return v.Len() >= n
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkLenMax(v reflect.Value, n int) bool {
|
|
switch v.Kind() {
|
|
case reflect.String, reflect.Slice, reflect.Map, reflect.Array:
|
|
return v.Len() <= n
|
|
}
|
|
return true
|
|
}
|