Previously hb_jsonDecode took only (cJSON) and returned the value.
That covers most uses but not the Harbour-spec second form
nBytesParsed := hb_jsonDecode( cJSON, @xOut )
which mod_harbour / fivenode PRG (e.g. bridge_context.prg's
ctx_get / ctx_set) and any other code that wants the parse-length
relies on. The byref output was silently dropped, so a hash lookup
went through the @hOut path that was always NIL and fell back to
the default value — looking like a hash key was missing even
though the JSON parsed fine.
Now PCount() == 1 keeps the legacy return-value form; PCount() >= 2
writes the decoded value into local-2 via SetLocal (which is
already byref-aware) and returns the byte count (0 on parse error).
Verified: hb_jsonDecode('{"x":1,"y":2}', @h) writes the hash and
returns 13; the 1-arg form still returns the value as before;
Compat 56/56 + go test ./compiler/... ./hbrt/... ./hbrtl/... all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
399 lines
9.1 KiB
Go
399 lines
9.1 KiB
Go
// 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()
|
|
}
|