# 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](/docs/api/surf.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.
