Refactor assistant data handling to improve JSON serialization
- Consolidate JSON serialization for assistant fields into a single function, `marshalJSONFields`, to streamline the code and improve maintainability.
- Introduce a new utility function, `isNil`, to accurately check for nil values, including typed nils in interface{} types.
- Remove redundant marshaling logic for options, tags, and modes, enhancing clarity and reducing code duplication.
- Update the `UpdateAssistant` method to utilize the new nil-checking logic for JSON fields, ensuring correct handling of nil values.
This commit is contained in:
parent
e8720f0421
commit
1b60feacab
4 changed files with 608 additions and 410 deletions
|
|
@ -145,31 +145,6 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
data["__yao_tenant_id"] = nil
|
||||
}
|
||||
|
||||
// Handle simple types
|
||||
if assistant.Options != nil {
|
||||
jsonStr, err := jsoniter.MarshalToString(assistant.Options)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal options: %w", err)
|
||||
}
|
||||
data["options"] = jsonStr
|
||||
}
|
||||
|
||||
if assistant.Tags != nil {
|
||||
jsonStr, err := jsoniter.MarshalToString(assistant.Tags)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal tags: %w", err)
|
||||
}
|
||||
data["tags"] = jsonStr
|
||||
}
|
||||
|
||||
if assistant.Modes != nil {
|
||||
jsonStr, err := jsoniter.MarshalToString(assistant.Modes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal modes: %w", err)
|
||||
}
|
||||
data["modes"] = jsonStr
|
||||
}
|
||||
|
||||
// DefaultMode is a simple string field
|
||||
if assistant.DefaultMode != "" {
|
||||
data["default_mode"] = assistant.DefaultMode
|
||||
|
|
@ -177,8 +152,12 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
data["default_mode"] = nil
|
||||
}
|
||||
|
||||
// Handle interface{} fields - they should already be in the correct format
|
||||
// Handle all JSON fields uniformly via marshalJSONFields.
|
||||
// Uses isNil() to correctly skip typed nils stored in interface{}.
|
||||
jsonFields := map[string]interface{}{
|
||||
"options": assistant.Options,
|
||||
"tags": assistant.Tags,
|
||||
"modes": assistant.Modes,
|
||||
"prompts": assistant.Prompts,
|
||||
"prompt_presets": assistant.PromptPresets,
|
||||
"connector_options": assistant.ConnectorOptions,
|
||||
|
|
@ -194,14 +173,8 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
"dependencies": assistant.Dependencies,
|
||||
}
|
||||
|
||||
for field, value := range jsonFields {
|
||||
if value != nil {
|
||||
jsonStr, err := jsoniter.MarshalToString(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal %s: %w", field, err)
|
||||
}
|
||||
data[field] = jsonStr
|
||||
}
|
||||
if err := marshalJSONFields(data, jsonFields); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update or insert
|
||||
|
|
@ -272,14 +245,14 @@ func (store *Xun) UpdateAssistant(assistantID string, updates map[string]interfa
|
|||
|
||||
// Handle JSON fields
|
||||
if jsonFieldSet[key] {
|
||||
if value != nil {
|
||||
if isNil(value) {
|
||||
data[key] = nil
|
||||
} else {
|
||||
jsonStr, err := jsoniter.MarshalToString(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal %s: %w", key, err)
|
||||
}
|
||||
data[key] = jsonStr
|
||||
} else {
|
||||
data[key] = nil
|
||||
}
|
||||
} else {
|
||||
// Handle regular fields
|
||||
|
|
|
|||
|
|
@ -2,9 +2,44 @@ package xun
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// isNil checks whether a value is truly nil, handling the Go typed-nil-in-interface pitfall.
|
||||
// A nil map, slice, or pointer stored in an interface{} is not == nil in Go;
|
||||
// this helper uses reflect to detect that case.
|
||||
func isNil(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Interface, reflect.Chan, reflect.Func:
|
||||
return rv.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// marshalJSONFields serialises each value in fields to a JSON string and writes
|
||||
// it into data. Truly-nil values (including typed nils) are skipped so the
|
||||
// database column keeps its SQL NULL / default.
|
||||
func marshalJSONFields(data map[string]interface{}, fields map[string]interface{}) error {
|
||||
for field, value := range fields {
|
||||
if isNil(value) {
|
||||
continue
|
||||
}
|
||||
jsonStr, err := jsoniter.MarshalToString(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal %s: %w", field, err)
|
||||
}
|
||||
data[field] = jsonStr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions for type conversion
|
||||
func getString(data map[string]interface{}, key string) string {
|
||||
if v, ok := data[key].(string); ok {
|
||||
|
|
|
|||
190
agent/store/xun/utils_test.go
Normal file
190
agent/store/xun/utils_test.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package xun
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testStruct struct{ Name string }
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
// Untyped nil
|
||||
t.Run("UntypedNil", func(t *testing.T) {
|
||||
assert.True(t, isNil(nil))
|
||||
})
|
||||
|
||||
// Typed nil pointer
|
||||
t.Run("TypedNilPointer", func(t *testing.T) {
|
||||
var p *testStruct
|
||||
assert.True(t, isNil(p))
|
||||
})
|
||||
|
||||
// Typed nil map
|
||||
t.Run("TypedNilMap", func(t *testing.T) {
|
||||
var m map[string]string
|
||||
assert.True(t, isNil(m))
|
||||
})
|
||||
|
||||
// Typed nil slice
|
||||
t.Run("TypedNilSlice", func(t *testing.T) {
|
||||
var s []string
|
||||
assert.True(t, isNil(s))
|
||||
})
|
||||
|
||||
// Non-nil pointer
|
||||
t.Run("NonNilPointer", func(t *testing.T) {
|
||||
p := &testStruct{Name: "test"}
|
||||
assert.False(t, isNil(p))
|
||||
})
|
||||
|
||||
// Non-nil map (empty)
|
||||
t.Run("NonNilEmptyMap", func(t *testing.T) {
|
||||
m := map[string]string{}
|
||||
assert.False(t, isNil(m))
|
||||
})
|
||||
|
||||
// Non-nil map with values
|
||||
t.Run("NonNilMap", func(t *testing.T) {
|
||||
m := map[string]string{"a": "1"}
|
||||
assert.False(t, isNil(m))
|
||||
})
|
||||
|
||||
// Non-nil slice (empty)
|
||||
t.Run("NonNilEmptySlice", func(t *testing.T) {
|
||||
s := []string{}
|
||||
assert.False(t, isNil(s))
|
||||
})
|
||||
|
||||
// Non-nil slice with values
|
||||
t.Run("NonNilSlice", func(t *testing.T) {
|
||||
s := []string{"a"}
|
||||
assert.False(t, isNil(s))
|
||||
})
|
||||
|
||||
// Scalar types (never nil)
|
||||
t.Run("String", func(t *testing.T) {
|
||||
assert.False(t, isNil("hello"))
|
||||
})
|
||||
t.Run("EmptyString", func(t *testing.T) {
|
||||
assert.False(t, isNil(""))
|
||||
})
|
||||
t.Run("Int", func(t *testing.T) {
|
||||
assert.False(t, isNil(42))
|
||||
})
|
||||
t.Run("Bool", func(t *testing.T) {
|
||||
assert.False(t, isNil(false))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarshalJSONFields(t *testing.T) {
|
||||
t.Run("SkipUntypedNil", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"field1": nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, exists := data["field1"]
|
||||
assert.False(t, exists, "untyped nil should be skipped")
|
||||
})
|
||||
|
||||
t.Run("SkipTypedNilMap", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
var m map[string]string
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"deps": m,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, exists := data["deps"]
|
||||
assert.False(t, exists, "typed nil map should be skipped")
|
||||
})
|
||||
|
||||
t.Run("SkipTypedNilSlice", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
var s []string
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"tags": s,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, exists := data["tags"]
|
||||
assert.False(t, exists, "typed nil slice should be skipped")
|
||||
})
|
||||
|
||||
t.Run("SkipTypedNilPointer", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
var p *testStruct
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"kb": p,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, exists := data["kb"]
|
||||
assert.False(t, exists, "typed nil pointer should be skipped")
|
||||
})
|
||||
|
||||
t.Run("MarshalNonNilMap", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"deps": map[string]string{"echo": "^1.0.0"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"echo":"^1.0.0"}`, data["deps"])
|
||||
})
|
||||
|
||||
t.Run("MarshalEmptyMap", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"deps": map[string]string{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{}`, data["deps"])
|
||||
})
|
||||
|
||||
t.Run("MarshalSlice", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"tags": []string{"ai", "bot"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `["ai","bot"]`, data["tags"])
|
||||
})
|
||||
|
||||
t.Run("MarshalPointer", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"kb": &testStruct{Name: "test"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"Name":"test"}`, data["kb"])
|
||||
})
|
||||
|
||||
t.Run("MixedNilAndNonNil", func(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
var nilMap map[string]string
|
||||
var nilSlice []string
|
||||
var nilPtr *testStruct
|
||||
|
||||
err := marshalJSONFields(data, map[string]interface{}{
|
||||
"nil_map": nilMap,
|
||||
"nil_slice": nilSlice,
|
||||
"nil_ptr": nilPtr,
|
||||
"nil_raw": nil,
|
||||
"good_map": map[string]string{"k": "v"},
|
||||
"good_list": []string{"a"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, data, 2, "only non-nil fields should be written")
|
||||
assert.Equal(t, `{"k":"v"}`, data["good_map"])
|
||||
assert.Equal(t, `["a"]`, data["good_list"])
|
||||
|
||||
_, exists := data["nil_map"]
|
||||
assert.False(t, exists)
|
||||
_, exists = data["nil_slice"]
|
||||
assert.False(t, exists)
|
||||
_, exists = data["nil_ptr"]
|
||||
assert.False(t, exists)
|
||||
_, exists = data["nil_raw"]
|
||||
assert.False(t, exists)
|
||||
})
|
||||
}
|
||||
746
data/bindata.go
746
data/bindata.go
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue