Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)
This commit is contained in:
87
internal/skill/jsonyaml.go
Normal file
87
internal/skill/jsonyaml.go
Normal file
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user