# wire JSON response helpers and value types that keep API bodies byte-compatible with a PHP (WinterCMS/Laravel) backend. `import "git.golem15.com/golem15/summercms/modules/wire"` ## Overview `wire` is the lowest layer of the HTTP stack: it decides how a Go value becomes response bytes. Its helpers reproduce what `json_encode` and Carbon produce in a WinterCMS or Laravel app (unescaped HTML characters, no trailing newline, `+00:00` timestamps, nullable booleans, `[]` rather than `null` for empty lists), so an endpoint ported from PHP returns the same body its existing clients already parse. [surf](../surf/README.md) uses it for the opaque 500 response of its panic recovery, and application handlers use it directly for their JSON responses. ## Features - `wire.WriteJSON` encodes a value with HTML escaping disabled and without the trailing newline `encoding/json` adds, then sets `Content-Type: application/json` and the status code. If encoding fails, it writes the opaque 500 body instead of a partial response. - `wire.WriteOpaque500` writes a 500 response with the fixed body `{"error":true,"message":"Internal server error"}`, which reveals nothing about the failure. - `wire.Time` wraps `time.Time` and always marshals in UTC as `2006-01-02T15:04:05+00:00` (Carbon's form, never Go's `Z`). It unmarshals a timestamp with a numeric offset or an RFC 3339 `Z` timestamp, and turns `null` into the zero time. - `wire.TriBool` models a nullable boolean: when `wire.TriBool.Valid` is false it marshals as `null`, otherwise as `wire.TriBool.Value`. - `wire.Slice` returns a non-nil empty slice for a nil input, so optional lists marshal as `[]` instead of `null`. ## Usage ```go package blog import ( "net/http" "time" "git.golem15.com/golem15/summercms/modules/wire" ) type postJSON struct { ID uint `json:"id"` Title string `json:"title"` Tags []string `json:"tags"` Featured wire.TriBool `json:"featured"` PublishedAt wire.Time `json:"published_at"` } func showPost(w http.ResponseWriter, r *http.Request) { var tags []string // nil when the post has no tags body := postJSON{ ID: 1, Title: "Hello & welcome", // "&" stays unescaped Tags: wire.Slice(tags), // marshals as [] Featured: wire.TriBool{}, // marshals as null PublishedAt: wire.Time{Time: time.Now()}, // "...+00:00" } wire.WriteJSON(w, http.StatusOK, map[string]any{"data": body}) } ``` ## API reference | Identifier | Description | |------------|-------------| | `wire.WriteJSON` | Writes a JSON body with HTML escaping off and no trailing newline; falls back to the opaque 500 on an encoding error. | | `wire.WriteOpaque500` | Writes the fixed `{"error":true,"message":"Internal server error"}` 500 response. | | `wire.Time` | `time.Time` wrapper that marshals as UTC `+00:00` and reads both `+00:00` and `Z` forms. | | `wire.TriBool` | Nullable boolean: `wire.TriBool.Valid` false marshals `null`, otherwise `wire.TriBool.Value`. | | `wire.Slice` | Generic helper that turns a nil slice into an empty one so it marshals as `[]`. | ## Dependencies - SummerCMS modules: none. - Third-party: none. - Standard library: `bytes`, `encoding/json`, `net/http`, `time`. ## Testing ```sh go test ./modules/wire/... ``` The tests use `net/http/httptest` and need no external services.