- WriteJSON matches genre_controller byte-for-byte (SetEscapeHTML false, trim trailing newline) - Time marshals Carbon +00:00 never Z; TriBool nulls when invalid; Slice never returns nil
91 lines
2.1 KiB
Go
91 lines
2.1 KiB
Go
package wire
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// WriteJSON encodes v as JSON with HTML escaping disabled and no trailing
|
|
// newline, matching the PHP json_encode body shape used by house handlers.
|
|
func WriteJSON(w http.ResponseWriter, status int, v any) {
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(v); err != nil {
|
|
WriteOpaque500(w)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write(bytes.TrimSuffix(buf.Bytes(), []byte("\n")))
|
|
}
|
|
|
|
// WriteOpaque500 writes the house opaque 500 body.
|
|
func WriteOpaque500(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":true,"message":"Internal server error"}`))
|
|
}
|
|
|
|
// Time marshals as Carbon's +00:00 form (2006-01-02T15:04:05+00:00), never
|
|
// Go's default "Z". Unmarshal accepts both +00:00 and Z for read paths.
|
|
type Time struct{ time.Time }
|
|
|
|
const (
|
|
carbonLayout = "2006-01-02T15:04:05-07:00"
|
|
rfc3339Z = time.RFC3339
|
|
)
|
|
|
|
func (t Time) MarshalJSON() ([]byte, error) {
|
|
s := t.UTC().Format("2006-01-02T15:04:05") + "+00:00"
|
|
return []byte(`"` + s + `"`), nil
|
|
}
|
|
|
|
func (t *Time) UnmarshalJSON(b []byte) error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
if string(b) == "null" {
|
|
t.Time = time.Time{}
|
|
return nil
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(b, &s); err != nil {
|
|
return err
|
|
}
|
|
var err error
|
|
for _, layout := range []string{carbonLayout, rfc3339Z} {
|
|
var parsed time.Time
|
|
parsed, err = time.Parse(layout, s)
|
|
if err == nil {
|
|
t.Time = parsed
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// TriBool keeps PHP's nullable-boolean tri-state: Valid=false marshals null.
|
|
type TriBool struct{ Valid, Value bool }
|
|
|
|
func (b TriBool) MarshalJSON() ([]byte, error) {
|
|
if !b.Valid {
|
|
return []byte("null"), nil
|
|
}
|
|
if b.Value {
|
|
return []byte("true"), nil
|
|
}
|
|
return []byte("false"), nil
|
|
}
|
|
|
|
// Slice returns s unchanged if non-nil, else a non-nil empty slice that
|
|
// marshals to [].
|
|
func Slice[T any](s []T) []T {
|
|
if s == nil {
|
|
return []T{}
|
|
}
|
|
return s
|
|
}
|