Skip to content

Commit 970eda4

Browse files
committed
fix: only validate exported struct fields
1 parent 4528796 commit 970eda4

2 files changed

Lines changed: 195 additions & 2 deletions

File tree

utils.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ func IsConvertibleToJs(rType reflect.Type, visited map[reflect.Type]bool, detail
141141
case reflect.Struct:
142142
for i := range rType.NumField() {
143143
field := rType.Field(i)
144+
jsonTagName, _, _ := strings.Cut(field.Tag.Get("json"), ",")
144145

145-
err := IsConvertibleToJs(field.Type, visited, detail)
146-
if err != nil {
146+
if !field.IsExported() || jsonTagName == "-" {
147+
continue
148+
}
149+
150+
if err := IsConvertibleToJs(field.Type, visited, detail); err != nil {
147151
return newGoToJsErr(GetGoTypeName(rType)+"."+field.Name, err)
148152
}
149153
}

utils_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package qjs_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/fastschema/qjs"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestIsConvertibleToJs(t *testing.T) {
12+
t.Run("BasicTypes", func(t *testing.T) {
13+
basicTypes := []struct {
14+
name string
15+
value any
16+
expected bool
17+
}{
18+
{"int", 42, true},
19+
{"string", "hello", true},
20+
{"bool", true, true},
21+
{"float64", 3.14, true},
22+
{"[]int", []int{1, 2, 3}, true},
23+
{"map[string]int", map[string]int{"key": 1}, true},
24+
}
25+
26+
for _, tt := range basicTypes {
27+
t.Run(tt.name, func(t *testing.T) {
28+
err := qjs.IsConvertibleToJs(reflect.TypeOf(tt.value), make(map[reflect.Type]bool), "test")
29+
if tt.expected {
30+
assert.NoError(t, err, "Expected %s to be convertible", tt.name)
31+
} else {
32+
assert.Error(t, err, "Expected %s to not be convertible", tt.name)
33+
}
34+
})
35+
}
36+
})
37+
38+
t.Run("UnsupportedTypes", func(t *testing.T) {
39+
t.Run("channel", func(t *testing.T) {
40+
err := qjs.IsConvertibleToJs(reflect.TypeOf(make(chan int)), make(map[reflect.Type]bool), "test")
41+
assert.Error(t, err, "Channel should not be convertible")
42+
assert.Contains(t, err.Error(), "channel", "Error should mention channel")
43+
})
44+
45+
// Note: Testing unsafe.Pointer directly is tricky in unit tests since (*int)(nil)
46+
// is treated as *int, not unsafe.Pointer. The unsafe.Pointer case is handled
47+
// in the switch statement but requires actual unsafe.Pointer type to trigger.
48+
})
49+
50+
t.Run("StructWithFields", func(t *testing.T) {
51+
t.Run("ExportedFields", func(t *testing.T) {
52+
type TestStruct struct {
53+
PublicField string
54+
AnotherField int
55+
}
56+
57+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
58+
assert.NoError(t, err, "Struct with exported fields should be convertible")
59+
})
60+
61+
t.Run("UnexportedFieldsIgnored", func(t *testing.T) {
62+
type TestStruct struct {
63+
PublicField string
64+
privateField string // Should be ignored
65+
}
66+
67+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
68+
assert.NoError(t, err, "Struct with unexported fields should be convertible (unexported fields ignored)")
69+
})
70+
71+
t.Run("MixedFieldsWithUnsupportedPrivate", func(t *testing.T) {
72+
type TestStruct struct {
73+
PublicField string
74+
privateField chan int // Unsupported type but private, should be ignored
75+
}
76+
77+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
78+
assert.NoError(t, err, "Struct should be convertible when unsupported types are in unexported fields")
79+
})
80+
})
81+
82+
t.Run("StructWithJSONTags", func(t *testing.T) {
83+
t.Run("JSONOmitTag", func(t *testing.T) {
84+
type TestStruct struct {
85+
PublicField string
86+
OmittedField chan int `json:"-"` // Should be ignored due to json:"-"
87+
AnotherField int
88+
}
89+
90+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
91+
assert.NoError(t, err, "Struct should be convertible when unsupported types have json:\"-\" tag")
92+
})
93+
94+
t.Run("JSONRenameTag", func(t *testing.T) {
95+
type TestStruct struct {
96+
Field string `json:"renamed_field"`
97+
UnsupportedField chan int `json:"-"`
98+
}
99+
100+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
101+
assert.NoError(t, err, "Struct should be convertible with JSON rename tags")
102+
})
103+
104+
t.Run("UnsupportedFieldWithoutOmitTag", func(t *testing.T) {
105+
type TestStruct struct {
106+
PublicField string
107+
UnsupportedField chan int // Should cause error since it's exported and not omitted
108+
}
109+
110+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
111+
assert.Error(t, err, "Struct should not be convertible with exported unsupported fields")
112+
assert.Contains(t, err.Error(), "TestStruct.UnsupportedField", "Error should mention the problematic field")
113+
})
114+
115+
t.Run("ComplexJSONTags", func(t *testing.T) {
116+
type TestStruct struct {
117+
Field1 string `json:"field1,omitempty"`
118+
Field2 int `json:"field2"`
119+
OmittedField chan int `json:"-"`
120+
AnotherOmitted chan bool `json:"-,"`
121+
}
122+
123+
err := qjs.IsConvertibleToJs(reflect.TypeOf(TestStruct{}), make(map[reflect.Type]bool), "test")
124+
assert.NoError(t, err, "Struct should handle complex JSON tags correctly")
125+
})
126+
})
127+
128+
t.Run("RecursiveTypes", func(t *testing.T) {
129+
type RecursiveStruct struct {
130+
Name string
131+
Self *RecursiveStruct
132+
}
133+
134+
err := qjs.IsConvertibleToJs(reflect.TypeOf(RecursiveStruct{}), make(map[reflect.Type]bool), "test")
135+
assert.NoError(t, err, "Recursive struct should be convertible")
136+
})
137+
138+
t.Run("NestedStructs", func(t *testing.T) {
139+
t.Run("ValidNested", func(t *testing.T) {
140+
type Inner struct {
141+
Value int
142+
}
143+
type Outer struct {
144+
Inner Inner
145+
Name string
146+
}
147+
148+
err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test")
149+
assert.NoError(t, err, "Nested convertible structs should be convertible")
150+
})
151+
152+
t.Run("InvalidNested", func(t *testing.T) {
153+
type Inner struct {
154+
BadField chan int // Unsupported
155+
}
156+
type Outer struct {
157+
Inner Inner
158+
Name string
159+
}
160+
161+
err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test")
162+
assert.Error(t, err, "Nested struct with unsupported fields should not be convertible")
163+
})
164+
165+
t.Run("NestedWithOmittedField", func(t *testing.T) {
166+
type Inner struct {
167+
GoodField string
168+
BadField chan int `json:"-"`
169+
}
170+
type Outer struct {
171+
Inner Inner
172+
Name string
173+
}
174+
175+
err := qjs.IsConvertibleToJs(reflect.TypeOf(Outer{}), make(map[reflect.Type]bool), "test")
176+
assert.NoError(t, err, "Nested struct with omitted unsupported fields should be convertible")
177+
})
178+
})
179+
180+
t.Run("Pointers", func(t *testing.T) {
181+
type TestStruct struct {
182+
Field string
183+
OmittedField chan int `json:"-"`
184+
}
185+
186+
err := qjs.IsConvertibleToJs(reflect.TypeOf(&TestStruct{}), make(map[reflect.Type]bool), "test")
187+
assert.NoError(t, err, "Pointer to convertible struct should be convertible")
188+
})
189+
}

0 commit comments

Comments
 (0)