Files
summercms/modules/wire
Jakub Zych efb35a2d35 feat(11.1-04): add the Database section and the core Services pages
- docs/database: models, migrations, queries and pagination, relations,
  casts and validation, attachments and transactions (lagoon.Transaction,
  lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase)
- docs/services: configuration, events, routing with auth groups, rate
  limiting, authentication, the OAuth server, mail and localization
- runnable Examples for lagoon, attach, compass, surf, wire, bouncer,
  wristband, postcard, phrasebook and festival; lagoon TestDocs* regions
  run on the package's Postgres harness through DocsDB
- 15 new required pages
2026-09-30 22:59:25 +02:00
..

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 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

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

go test ./modules/wire/...

The tests use net/http/httptest and need no external services.