package main
import (
"fmt"
"net/http"
"strings"
)
// jsonParse renders any JSON body as tables, splitting nested objects into
// their own tables under path breadcrumbs like order->items->[0]->tags.
func jsonParse(raw []byte, headers http.Header, meta Meta) (string, string, bool) {
v, err := decodeJSON(raw)
if err != nil {
return "", "", false
}
body := section("📋 Request Information", table(
row("IP Address", h(meta.IP)),
row("Method", h(meta.Method)),
row("Content-Type", h(meta.ContentType)),
)) + section("📦 JSON Data", renderJSON(v, ""))
html := emailShell("📊 JSON Webhook Data", h(meta.Date), body, "JSON data automatically parsed and formatted.")
return html, "📊 JSON Webhook - " + meta.Date, true
}
func pathTitle(path string) string {
if path == "" {
return ""
}
return `
` + h(path) + `
`
}
func renderJSON(data any, path string) string {
var b strings.Builder
switch d := data.(type) {
case []any:
if len(d) == 0 {
return ""
}
allScalar := true
for _, it := range d {
if isContainer(it) {
allScalar = false
break
}
}
if allScalar {
// Array of plain values: one table under a single breadcrumb
rows := make([]string, 0, len(d))
for i, it := range d {
rows = append(rows, row("Item "+fmt.Sprint(i+1), esc(str(it))))
}
return pathTitle(path) + table(rows...)
}
for i, item := range d {
itemPath := fmt.Sprintf("[%d]", i)
if path != "" {
itemPath = path + "->" + itemPath
}
if m := asObject(item); m != nil {
var simple []string
var sub []string
for _, k := range m.Keys {
if isContainer(m.M[k]) {
sub = append(sub, k)
} else {
simple = append(simple, row(h(k), esc(str(m.M[k]))))
}
}
if len(simple) > 0 {
b.WriteString(pathTitle(path2(path, itemPath)))
b.WriteString(`Item ` + fmt.Sprint(i+1) + `
` + table(simple...) + `
`)
}
for _, k := range sub {
b.WriteString(renderJSON(m.M[k], itemPath+"->"+k))
}
} else if isContainer(item) {
b.WriteString(renderJSON(item, itemPath))
} else {
b.WriteString(table(row("Item "+fmt.Sprint(i+1), esc(str(item)))))
}
}
case *OMap:
var simple []string
var sub []string
for _, k := range d.Keys {
if isContainer(d.M[k]) {
sub = append(sub, k)
} else {
simple = append(simple, row(h(k), esc(str(d.M[k]))))
}
}
if len(simple) > 0 {
b.WriteString(pathTitle(path) + table(simple...))
}
for _, k := range sub {
subPath := k
if path != "" {
subPath = path + "->" + k
}
b.WriteString(renderJSON(d.M[k], subPath))
}
default:
// Scalar (or null): one-row table so it lines up with everything else
label := path
if label == "" {
label = "value"
}
val := str(data)
if data == nil {
val = "null"
}
return table(row(h(label), esc(val)))
}
return b.String()
}
// path2 mirrors the PHP behaviour: an item breadcrumb is only shown when nested under a path.
func path2(parent, itemPath string) string {
if parent == "" {
return ""
}
return itemPath
}