diff --git a/api.go b/api.go index 49f7f41..a5a6288 100644 --- a/api.go +++ b/api.go @@ -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. diff --git a/docs/RFC.md b/docs/RFC.md index 3c6f15c..39df9d2 100644 --- a/docs/RFC.md +++ b/docs/RFC.md @@ -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) ``` --- diff --git a/json.go b/json.go new file mode 100644 index 0000000..65e119f --- /dev/null +++ b/json.go @@ -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) +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..6841e39 --- /dev/null +++ b/json_test.go @@ -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) +}