// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // JSON functions — Harbour compatible API + Go-native extensions. // // Harbour compatible: // hb_jsonEncode(xValue [, lHumanReadable]) → cJSON // hb_jsonDecode(cJSON) → xValue // // Five extensions (Go-native, beyond Harbour): // JsonPretty(xValue [, cIndent]) → cPrettyJSON // JsonTo(xValue, cFile) → lSuccess // JsonFrom(cFile) → xValue // JsonPath(xValue, cPath) → xResult — $.key.sub[0] // JsonMerge(hDest, hSrc) → hMerged — deep merge // JsonType(cJSON) → cType // JsonValid(cJSON) → lValid // JsonHttpGet(cURL [, nTimeout]) → hResult — HTTP GET + JSON // JsonHttpPost(cURL, xBody [, nTimeout]) — HTTP POST + JSON package hbrtl import ( "encoding/json" "five/hbrt" "io" "net/http" "os" "strconv" "strings" "time" ) // === Harbour Compatible === // HB_JSONENCODE(xValue [, lHumanReadable]) → cJSON func HbJsonEncode(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() v := t.Local(1) pretty := false if nParams >= 2 && !t.Local(2).IsNil() { pretty = t.Local(2).AsBool() } goVal := valueToGo(v) var data []byte var err error if pretty { data, err = json.MarshalIndent(goVal, "", " ") } else { data, err = json.Marshal(goVal) } if err != nil { t.RetString("") return } t.RetString(string(data)) } // HB_JSONDECODE(cJSON [, @xOut]) → xValue | nBytesParsed // // Two Harbour-spec calling forms: // // - 1 arg : returns the decoded value (or NIL on parse error). // - 2 args: writes the decoded value into the byref @xOut and // returns the number of bytes parsed (0 on error). This is the // form mod_harbour / fivenode PRG code uses for ctx_get and the // like. Five previously implemented only the 1-arg form, so any // PRG that relied on the byref output saw NIL and silently fell // through to a default. func HbJsonDecode(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() s := t.Local(1).AsString() var raw interface{} if err := json.Unmarshal([]byte(s), &raw); err != nil { if nParams >= 2 { t.SetLocal(2, hbrt.MakeNil()) t.RetInt(0) } else { t.RetNil() } return } v := goToValue(raw) if nParams >= 2 { t.SetLocal(2, v) t.RetInt(int64(len(s))) return } t.RetVal(v) } // === Five Extensions === // JSONPRETTY(xValue [, cIndent]) → cPrettyJSON func JsonPretty(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() indent := " " if nParams >= 2 && !t.Local(2).IsNil() { indent = t.Local(2).AsString() } data, _ := json.MarshalIndent(valueToGo(t.Local(1)), "", indent) t.RetString(string(data)) } // JSONTO(xValue, cFile) → lSuccess func JsonTo(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() data, err := json.MarshalIndent(valueToGo(t.Local(1)), "", " ") if err != nil { t.RetBool(false) return } t.RetBool(os.WriteFile(t.Local(2).AsString(), data, 0644) == nil) } // JSONFROM(cFile) → xValue func JsonFrom(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() data, err := os.ReadFile(t.Local(1).AsString()) if err != nil { t.RetNil() return } var raw interface{} if json.Unmarshal(data, &raw) != nil { t.RetNil() return } t.RetVal(goToValue(raw)) } // JSONPATH(xValue, cPath) → xResult — $.key.sub[0] func JsonPath(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() root := t.Local(1) path := strings.TrimPrefix(strings.TrimPrefix(t.Local(2).AsString(), "$."), "$") if path == "" { t.RetVal(root) return } t.RetVal(navigatePath(root, path)) } func navigatePath(v hbrt.Value, path string) hbrt.Value { for _, part := range splitPath(path) { if v.IsNil() { return hbrt.MakeNil() } if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { idx, _ := strconv.Atoi(part[1 : len(part)-1]) if v.IsArray() { arr := v.AsArray() if idx >= 0 && idx < len(arr.Items) { v = arr.Items[idx] continue } } return hbrt.MakeNil() } if v.IsHash() { h := v.AsHash() if i := h.Lookup(hbrt.MakeString(part)); i >= 0 { v = h.Values[i] } else { return hbrt.MakeNil() } } else { return hbrt.MakeNil() } } return v } func splitPath(path string) []string { var parts []string cur := "" for i := 0; i < len(path); i++ { if path[i] == '.' { if cur != "" { parts = append(parts, cur) cur = "" } } else if path[i] == '[' { if cur != "" { parts = append(parts, cur) cur = "" } j := i for j < len(path) && path[j] != ']' { j++ } parts = append(parts, path[i:j+1]) i = j } else { cur += string(path[i]) } } if cur != "" { parts = append(parts, cur) } return parts } // JSONMERGE(hDest, hSrc) → hMerged func JsonMerge(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() dest, src := t.Local(1), t.Local(2) if !dest.IsHash() || !src.IsHash() { t.RetVal(src) return } dh, sh := dest.AsHash(), src.AsHash() result := &hbrt.HbHash{ Keys: make([]hbrt.Value, len(dh.Keys)), Values: make([]hbrt.Value, len(dh.Values)), } copy(result.Keys, dh.Keys) copy(result.Values, dh.Values) for i, sk := range sh.Keys { result.Set(sk, sh.Values[i]) } t.RetVal(hbrt.MakeHashFrom(result)) } // JSONTYPE(cJSON) → "object"|"array"|"string"|"number"|"boolean"|"null"|"invalid" func JsonType(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() s := strings.TrimSpace(t.Local(1).AsString()) if s == "" { t.RetString("invalid") return } switch s[0] { case '{': t.RetString("object") case '[': t.RetString("array") case '"': t.RetString("string") case 't', 'f': t.RetString("boolean") case 'n': t.RetString("null") default: if (s[0] >= '0' && s[0] <= '9') || s[0] == '-' { t.RetString("number") } else { t.RetString("invalid") } } } // JSONVALID(cJSON) → lValid func JsonValid(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() t.RetBool(json.Valid([]byte(t.Local(1).AsString()))) } // JSONHTTPGET(cURL [, nTimeout]) → {"status":n, "body":c, "error":c} func JsonHttpGet(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() url := t.Local(1).AsString() timeout := 30 if nParams >= 2 && !t.Local(2).IsNil() { timeout = t.Local(2).AsInt() } client := &http.Client{Timeout: time.Duration(timeout) * time.Second} resp, err := client.Get(url) if err != nil { t.RetVal(makeHttpResult(0, "", err.Error())) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) t.RetVal(makeHttpResult(resp.StatusCode, string(body), "")) } // JSONHTTPPOST(cURL, xBody [, nTimeout]) → {"status":n, "body":c, "error":c} func JsonHttpPost(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() url := t.Local(1).AsString() bodyVal := t.Local(2) timeout := 30 if nParams >= 3 && !t.Local(3).IsNil() { timeout = t.Local(3).AsInt() } var bodyStr string if bodyVal.IsString() { bodyStr = bodyVal.AsString() } else { data, _ := json.Marshal(valueToGo(bodyVal)) bodyStr = string(data) } client := &http.Client{Timeout: time.Duration(timeout) * time.Second} resp, err := client.Post(url, "application/json", strings.NewReader(bodyStr)) if err != nil { t.RetVal(makeHttpResult(0, "", err.Error())) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) t.RetVal(makeHttpResult(resp.StatusCode, string(body), "")) } func makeHttpResult(status int, body, errMsg string) hbrt.Value { h := &hbrt.HbHash{ Keys: []hbrt.Value{hbrt.MakeString("status"), hbrt.MakeString("body"), hbrt.MakeString("error")}, Values: []hbrt.Value{hbrt.MakeInt(status), hbrt.MakeString(body), hbrt.MakeString(errMsg)}, } return hbrt.MakeHashFrom(h) } // === Conversion: Five Value ↔ Go interface{} === func valueToGo(v hbrt.Value) interface{} { if v.IsNil() { return nil } if v.IsLogical() { return v.AsBool() } if v.IsNumInt() { return v.AsNumInt() } if v.IsNumeric() { return v.AsNumDouble() } if v.IsString() { return v.AsString() } if v.IsArray() { arr := v.AsArray() result := make([]interface{}, len(arr.Items)) for i, item := range arr.Items { result[i] = valueToGo(item) } return result } if v.IsHash() { h := v.AsHash() result := make(map[string]interface{}, len(h.Keys)) for i := range h.Keys { result[h.Keys[i].AsString()] = valueToGo(h.Values[i]) } return result } return nil } func goToValue(v interface{}) hbrt.Value { if v == nil { return hbrt.MakeNil() } switch val := v.(type) { case bool: return hbrt.MakeBool(val) case float64: if val == float64(int64(val)) { return hbrt.MakeNumInt(int64(val)) } return hbrt.MakeDouble(val, 0, 0) case string: return hbrt.MakeString(val) case []interface{}: items := make([]hbrt.Value, len(val)) for i, item := range val { items[i] = goToValue(item) } return hbrt.MakeArrayFrom(items) case map[string]interface{}: h := &hbrt.HbHash{ Keys: make([]hbrt.Value, 0, len(val)), Values: make([]hbrt.Value, 0, len(val)), } for k, v := range val { h.Keys = append(h.Keys, hbrt.MakeString(k)) h.Values = append(h.Values, goToValue(v)) } return hbrt.MakeHashFrom(h) } return hbrt.MakeNil() }