- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
395 lines
8.8 KiB
Go
395 lines
8.8 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) → xValue
|
|
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 {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
t.RetVal(goToValue(raw))
|
|
}
|
|
|
|
// === 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()
|
|
found := false
|
|
for i, k := range h.Keys {
|
|
if k.AsString() == part {
|
|
v = h.Values[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
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 {
|
|
found := false
|
|
for j, rk := range result.Keys {
|
|
if rk.AsString() == sk.AsString() {
|
|
result.Values[j] = sh.Values[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
result.Keys = append(result.Keys, sk)
|
|
result.Values = append(result.Values, 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()
|
|
}
|