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:
parent
12adc97bbd
commit
8626710f9d
4 changed files with 128 additions and 20 deletions
21
api.go
21
api.go
|
|
@ -108,7 +108,7 @@ func (a *API) Call(endpoint string, action string, opts Options) Result {
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
||||||
// Encode the action call as JSON-RPC (MCP compatible)
|
// 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 {
|
if err := stream.Send([]byte(payload)); err != nil {
|
||||||
return Result{err, false}
|
return Result{err, false}
|
||||||
|
|
@ -139,25 +139,6 @@ func extractScheme(transport string) string {
|
||||||
return transport
|
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.
|
// 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.
|
// If the action name contains ":", the prefix is the endpoint and the suffix is the action.
|
||||||
|
|
|
||||||
|
|
@ -533,6 +533,12 @@ core.FilterArgs(args) // strip flags, keep positional
|
||||||
core.ID() // "id-42-a3f2b1" — unique per process
|
core.ID() // "id-42-a3f2b1" — unique per process
|
||||||
core.ValidateName("brain") // Result{OK: true} — rejects "", ".", "..", path seps
|
core.ValidateName("brain") // Result{OK: true} — rejects "", ".", "..", path seps
|
||||||
core.SanitisePath("../../x") // "x" — extracts safe base, "invalid" for dangerous
|
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
58
json.go
Normal 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
63
json_test.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue