Files
backend/internal/skill/jsonyaml.go

88 lines
2.5 KiB
Go
Raw Normal View History

package skill
import (
"encoding/json"
"fmt"
)
// jsonObject is a yaml-aware wrapper around arbitrary JSON. It decodes from
// both YAML and JSON maps and, on encode, emits itself as a proper YAML map
// rather than a byte/number array. This is needed because Tool.Parameters is
// a JSON Schema (json.RawMessage) that must round-trip through the markdown
// frontmatter as a readable nested object.
//
// We implement yaml.Node-based encode/decode indirectly: by converting to/from
// map[string]any at the boundaries the default yaml encoder produces the
// expected nested structure. For JSON (the HTTP API), the underlying JSON is
// passed through unchanged.
type jsonObject json.RawMessage
// MarshalJSON returns the underlying JSON bytes unchanged.
func (j jsonObject) MarshalJSON() ([]byte, error) {
if len(j) == 0 {
return []byte("null"), nil
}
return j, nil
}
// UnmarshalJSON stores the raw bytes verbatim.
func (j *jsonObject) UnmarshalJSON(b []byte) error {
*j = append((*j)[:0], b...)
return nil
}
// MarshalYAML converts the JSON to a generic value tree so the YAML encoder
// writes a nested map/sequence rather than a list of byte integers.
func (j jsonObject) MarshalYAML() (interface{}, error) {
if len(j) == 0 {
return nil, nil
}
var v interface{}
if err := json.Unmarshal(j, &v); err != nil {
return nil, fmt.Errorf("json object -> yaml: %w", err)
}
return v, nil
}
// UnmarshalYAML accepts a YAML node and re-encodes it as canonical JSON, so
// editors writing hand-typed JSON Schema in YAML form still parse correctly.
func (j *jsonObject) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v interface{}
if err := unmarshal(&v); err != nil {
return err
}
// Numeric keys are not valid in JSON Schema, but YAML may have produced
// map[interface{}]interface{}; normalise through JSON round-trip.
b, err := json.Marshal(normaliseYAMLMap(v))
if err != nil {
return err
}
*j = b
return nil
}
// normaliseYAMLMap recursively converts map[interface{}]interface{} (which
// yaml.v3 sometimes yields for nested structures) into map[string]any so the
// value is JSON-encodable.
func normaliseYAMLMap(v interface{}) interface{} {
switch m := v.(type) {
case map[interface{}]interface{}:
out := make(map[string]interface{}, len(m))
for k, val := range m {
out[fmt.Sprint(k)] = normaliseYAMLMap(val)
}
return out
case map[string]interface{}:
for k, val := range m {
m[k] = normaliseYAMLMap(val)
}
return m
case []interface{}:
for i, val := range m {
m[i] = normaliseYAMLMap(val)
}
return m
}
return v
}