feat: add JSON primitives + fix api.go placeholder

core.JSONMarshal(), JSONMarshalString(), JSONUnmarshal(), JSONUnmarshalString()
wrap encoding/json so consumers don't import it directly.
Same guardrail pattern as string.go wraps strings.

api.go Call() now uses JSONMarshalString instead of placeholder optionsToJSON.
7 AX-7 tests. 490 tests total, 84.8% coverage.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-25 17:40:55 +00:00
parent 12adc97bbd
commit 8626710f9d
4 changed files with 128 additions and 20 deletions

21
api.go
View file

@ -108,7 +108,7 @@ func (a *API) Call(endpoint string, action string, opts Options) Result {
defer stream.Close()
// Encode the action call as JSON-RPC (MCP compatible)
payload := Concat(`{"action":"`, action, `","options":`, optionsToJSON(opts), `}`)
payload := Concat(`{"action":"`, action, `","options":`, JSONMarshalString(opts), `}`)
if err := stream.Send([]byte(payload)); err != nil {
return Result{err, false}
@ -139,25 +139,6 @@ func extractScheme(transport string) string {
return transport
}
// optionsToJSON is a minimal JSON serialiser for Options.
// core/go stays stdlib-only — no encoding/json import.
func optionsToJSON(opts Options) string {
b := NewBuilder()
b.WriteString("{")
first := true
for i := 0; ; i++ {
r := opts.Get(Sprintf("_key_%d", i))
if !r.OK {
break
}
// This is a placeholder — real implementation needs proper iteration
_ = first
first = false
}
// Simple fallback: serialize known keys
b.WriteString("}")
return b.String()
}
// RemoteAction resolves "host:action.name" syntax for transparent remote dispatch.
// If the action name contains ":", the prefix is the endpoint and the suffix is the action.

View file

@ -533,6 +533,12 @@ core.FilterArgs(args) // strip flags, keep positional
core.ID() // "id-42-a3f2b1" — unique per process
core.ValidateName("brain") // Result{OK: true} — rejects "", ".", "..", path seps
core.SanitisePath("../../x") // "x" — extracts safe base, "invalid" for dangerous
// JSON (wraps encoding/json — consumers don't import it directly)
core.JSONMarshal(myStruct) // Result{Value: []byte, OK: bool}
core.JSONMarshalString(myStruct) // string (returns "{}" on error)
core.JSONUnmarshal(data, &target) // Result{OK: bool}
core.JSONUnmarshalString(s, &target)
```
---

58
json.go Normal file
View file

@ -0,0 +1,58 @@
// SPDX-License-Identifier: EUPL-1.2
// JSON helpers for the Core framework.
// Wraps encoding/json so consumers don't import it directly.
// Same guardrail pattern as string.go wraps strings.
//
// Usage:
//
// data := core.JSONMarshal(myStruct)
// if data.OK { json := data.Value.([]byte) }
//
// r := core.JSONUnmarshal(jsonBytes, &target)
// if !r.OK { /* handle error */ }
package core
import "encoding/json"
// JSONMarshal serialises a value to JSON bytes.
//
// r := core.JSONMarshal(myStruct)
// if r.OK { data := r.Value.([]byte) }
func JSONMarshal(v any) Result {
data, err := json.Marshal(v)
if err != nil {
return Result{err, false}
}
return Result{data, true}
}
// JSONMarshalString serialises a value to a JSON string.
//
// s := core.JSONMarshalString(myStruct)
func JSONMarshalString(v any) string {
data, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(data)
}
// JSONUnmarshal deserialises JSON bytes into a target.
//
// var cfg Config
// r := core.JSONUnmarshal(data, &cfg)
func JSONUnmarshal(data []byte, target any) Result {
if err := json.Unmarshal(data, target); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// JSONUnmarshalString deserialises a JSON string into a target.
//
// var cfg Config
// r := core.JSONUnmarshalString(`{"port":8080}`, &cfg)
func JSONUnmarshalString(s string, target any) Result {
return JSONUnmarshal([]byte(s), target)
}

63
json_test.go Normal file
View file

@ -0,0 +1,63 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
type testJSON struct {
Name string `json:"name"`
Port int `json:"port"`
}
// --- JSONMarshal ---
func TestJson_JSONMarshal_Good(t *testing.T) {
r := JSONMarshal(testJSON{Name: "brain", Port: 8080})
assert.True(t, r.OK)
assert.Contains(t, string(r.Value.([]byte)), `"name":"brain"`)
}
func TestJson_JSONMarshal_Bad_Unmarshalable(t *testing.T) {
r := JSONMarshal(make(chan int))
assert.False(t, r.OK)
}
// --- JSONMarshalString ---
func TestJson_JSONMarshalString_Good(t *testing.T) {
s := JSONMarshalString(testJSON{Name: "x", Port: 1})
assert.Contains(t, s, `"name":"x"`)
}
func TestJson_JSONMarshalString_Ugly_Fallback(t *testing.T) {
s := JSONMarshalString(make(chan int))
assert.Equal(t, "{}", s)
}
// --- JSONUnmarshal ---
func TestJson_JSONUnmarshal_Good(t *testing.T) {
var target testJSON
r := JSONUnmarshal([]byte(`{"name":"brain","port":8080}`), &target)
assert.True(t, r.OK)
assert.Equal(t, "brain", target.Name)
assert.Equal(t, 8080, target.Port)
}
func TestJson_JSONUnmarshal_Bad_Invalid(t *testing.T) {
var target testJSON
r := JSONUnmarshal([]byte(`not json`), &target)
assert.False(t, r.OK)
}
// --- JSONUnmarshalString ---
func TestJson_JSONUnmarshalString_Good(t *testing.T) {
var target testJSON
r := JSONUnmarshalString(`{"name":"x","port":1}`, &target)
assert.True(t, r.OK)
assert.Equal(t, "x", target.Name)
}