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:
Max 2026-03-02 14:51:13 +08:00
parent e8720f0421
commit 1b60feacab
4 changed files with 608 additions and 410 deletions

View file

@ -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

View file

@ -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 {

View 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)
})
}

File diff suppressed because it is too large Load diff