1package schema
2
3import (
4 "reflect"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestEnumSupport(t *testing.T) {
11 // Test enum via struct tags
12 type WeatherInput struct {
13 Location string `json:"location" description:"City name"`
14 Units string `json:"units" enum:"celsius,fahrenheit,kelvin" description:"Temperature units"`
15 Format string `json:"format,omitempty" enum:"json,xml,text"`
16 }
17
18 schema := Generate(reflect.TypeOf(WeatherInput{}))
19
20 require.Equal(t, "object", schema.Type)
21
22 // Check units field has enum values
23 unitsSchema := schema.Properties["units"]
24 require.NotNil(t, unitsSchema, "Expected units property to exist")
25 require.Len(t, unitsSchema.Enum, 3)
26 expectedUnits := []string{"celsius", "fahrenheit", "kelvin"}
27 for i, expected := range expectedUnits {
28 require.Equal(t, expected, unitsSchema.Enum[i])
29 }
30
31 // Check required fields (format should not be required due to omitempty)
32 expectedRequired := []string{"location", "units"}
33 require.Len(t, schema.Required, len(expectedRequired))
34}
35
36func TestSchemaToParameters(t *testing.T) {
37 testSchema := Schema{
38 Type: "object",
39 Properties: map[string]*Schema{
40 "name": {
41 Type: "string",
42 Description: "The name field",
43 },
44 "age": {
45 Type: "integer",
46 Minimum: func() *float64 { v := 0.0; return &v }(),
47 Maximum: func() *float64 { v := 120.0; return &v }(),
48 },
49 "tags": {
50 Type: "array",
51 Items: &Schema{
52 Type: "string",
53 },
54 },
55 "priority": {
56 Type: "string",
57 Enum: []any{"low", "medium", "high"},
58 },
59 },
60 Required: []string{"name"},
61 }
62
63 params := ToParameters(testSchema)
64
65 // Check name parameter
66 nameParam, ok := params["name"].(map[string]any)
67 require.True(t, ok, "Expected name parameter to exist")
68 require.Equal(t, "string", nameParam["type"])
69 require.Equal(t, "The name field", nameParam["description"])
70
71 // Check age parameter with min/max
72 ageParam, ok := params["age"].(map[string]any)
73 require.True(t, ok, "Expected age parameter to exist")
74 require.Equal(t, "integer", ageParam["type"])
75 require.Equal(t, 0.0, ageParam["minimum"])
76 require.Equal(t, 120.0, ageParam["maximum"])
77
78 // Check priority parameter with enum
79 priorityParam, ok := params["priority"].(map[string]any)
80 require.True(t, ok, "Expected priority parameter to exist")
81 require.Equal(t, "string", priorityParam["type"])
82 enumValues, ok := priorityParam["enum"].([]any)
83 require.True(t, ok)
84 require.Len(t, enumValues, 3)
85}
86
87func TestGenerateSchemaBasicTypes(t *testing.T) {
88 t.Parallel()
89
90 tests := []struct {
91 name string
92 input any
93 expected Schema
94 }{
95 {
96 name: "string type",
97 input: "",
98 expected: Schema{Type: "string"},
99 },
100 {
101 name: "int type",
102 input: 0,
103 expected: Schema{Type: "integer"},
104 },
105 {
106 name: "int64 type",
107 input: int64(0),
108 expected: Schema{Type: "integer"},
109 },
110 {
111 name: "uint type",
112 input: uint(0),
113 expected: Schema{Type: "integer"},
114 },
115 {
116 name: "float64 type",
117 input: 0.0,
118 expected: Schema{Type: "number"},
119 },
120 {
121 name: "float32 type",
122 input: float32(0.0),
123 expected: Schema{Type: "number"},
124 },
125 {
126 name: "bool type",
127 input: false,
128 expected: Schema{Type: "boolean"},
129 },
130 }
131
132 for _, tt := range tests {
133 t.Run(tt.name, func(t *testing.T) {
134 t.Parallel()
135 schema := Generate(reflect.TypeOf(tt.input))
136 require.Equal(t, tt.expected.Type, schema.Type)
137 })
138 }
139}
140
141func TestGenerateSchemaArrayTypes(t *testing.T) {
142 t.Parallel()
143
144 tests := []struct {
145 name string
146 input any
147 expected Schema
148 }{
149 {
150 name: "string slice",
151 input: []string{},
152 expected: Schema{
153 Type: "array",
154 Items: &Schema{Type: "string"},
155 },
156 },
157 {
158 name: "int slice",
159 input: []int{},
160 expected: Schema{
161 Type: "array",
162 Items: &Schema{Type: "integer"},
163 },
164 },
165 {
166 name: "string array",
167 input: [3]string{},
168 expected: Schema{
169 Type: "array",
170 Items: &Schema{Type: "string"},
171 },
172 },
173 }
174
175 for _, tt := range tests {
176 t.Run(tt.name, func(t *testing.T) {
177 t.Parallel()
178 schema := Generate(reflect.TypeOf(tt.input))
179 require.Equal(t, tt.expected.Type, schema.Type)
180 require.NotNil(t, schema.Items, "Expected items schema to exist")
181 require.Equal(t, tt.expected.Items.Type, schema.Items.Type)
182 })
183 }
184}
185
186func TestGenerateSchemaMapTypes(t *testing.T) {
187 t.Parallel()
188
189 tests := []struct {
190 name string
191 input any
192 expected string
193 }{
194 {
195 name: "string to string map",
196 input: map[string]string{},
197 expected: "object",
198 },
199 {
200 name: "string to int map",
201 input: map[string]int{},
202 expected: "object",
203 },
204 {
205 name: "int to string map",
206 input: map[int]string{},
207 expected: "object",
208 },
209 }
210
211 for _, tt := range tests {
212 t.Run(tt.name, func(t *testing.T) {
213 t.Parallel()
214 schema := Generate(reflect.TypeOf(tt.input))
215 require.Equal(t, tt.expected, schema.Type)
216 })
217 }
218}
219
220func TestGenerateSchemaStructTypes(t *testing.T) {
221 t.Parallel()
222
223 type SimpleStruct struct {
224 Name string `json:"name" description:"The name field"`
225 Age int `json:"age"`
226 }
227
228 type StructWithOmitEmpty struct {
229 Required string `json:"required"`
230 Optional string `json:"optional,omitempty"`
231 }
232
233 type StructWithJSONIgnore struct {
234 Visible string `json:"visible"`
235 Hidden string `json:"-"`
236 }
237
238 type StructWithoutJSONTags struct {
239 FirstName string
240 LastName string
241 }
242
243 tests := []struct {
244 name string
245 input any
246 validate func(t *testing.T, schema Schema)
247 }{
248 {
249 name: "simple struct",
250 input: SimpleStruct{},
251 validate: func(t *testing.T, schema Schema) {
252 require.Equal(t, "object", schema.Type)
253 require.Len(t, schema.Properties, 2)
254 require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
255 require.Equal(t, "The name field", schema.Properties["name"].Description)
256 require.Len(t, schema.Required, 2)
257 },
258 },
259 {
260 name: "struct with omitempty",
261 input: StructWithOmitEmpty{},
262 validate: func(t *testing.T, schema Schema) {
263 require.Len(t, schema.Required, 1)
264 require.Equal(t, "required", schema.Required[0])
265 },
266 },
267 {
268 name: "struct with json ignore",
269 input: StructWithJSONIgnore{},
270 validate: func(t *testing.T, schema Schema) {
271 require.Len(t, schema.Properties, 1)
272 require.NotNil(t, schema.Properties["visible"], "Expected visible property to exist")
273 require.Nil(t, schema.Properties["hidden"], "Expected hidden property to not exist")
274 },
275 },
276 {
277 name: "struct without json tags",
278 input: StructWithoutJSONTags{},
279 validate: func(t *testing.T, schema Schema) {
280 require.NotNil(t, schema.Properties["first_name"], "Expected first_name property to exist")
281 require.NotNil(t, schema.Properties["last_name"], "Expected last_name property to exist")
282 },
283 },
284 }
285
286 for _, tt := range tests {
287 t.Run(tt.name, func(t *testing.T) {
288 t.Parallel()
289 schema := Generate(reflect.TypeOf(tt.input))
290 tt.validate(t, schema)
291 })
292 }
293}
294
295func TestGenerateSchemaPointerTypes(t *testing.T) {
296 t.Parallel()
297
298 type StructWithPointers struct {
299 Name *string `json:"name"`
300 Age *int `json:"age"`
301 }
302
303 schema := Generate(reflect.TypeOf(StructWithPointers{}))
304
305 require.Equal(t, "object", schema.Type)
306
307 require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
308 require.Equal(t, "string", schema.Properties["name"].Type)
309
310 require.NotNil(t, schema.Properties["age"], "Expected age property to exist")
311 require.Equal(t, "integer", schema.Properties["age"].Type)
312}
313
314func TestGenerateSchemaNestedStructs(t *testing.T) {
315 t.Parallel()
316
317 type Address struct {
318 Street string `json:"street"`
319 City string `json:"city"`
320 }
321
322 type Person struct {
323 Name string `json:"name"`
324 Address Address `json:"address"`
325 }
326
327 schema := Generate(reflect.TypeOf(Person{}))
328
329 require.Equal(t, "object", schema.Type)
330
331 require.NotNil(t, schema.Properties["address"], "Expected address property to exist")
332
333 addressSchema := schema.Properties["address"]
334 require.Equal(t, "object", addressSchema.Type)
335
336 require.NotNil(t, addressSchema.Properties["street"], "Expected street property in address to exist")
337 require.NotNil(t, addressSchema.Properties["city"], "Expected city property in address to exist")
338}
339
340func TestGenerateSchemaRecursiveStructs(t *testing.T) {
341 t.Parallel()
342
343 type Node struct {
344 Value string `json:"value"`
345 Next *Node `json:"next,omitempty"`
346 }
347
348 schema := Generate(reflect.TypeOf(Node{}))
349
350 require.Equal(t, "object", schema.Type)
351
352 require.NotNil(t, schema.Properties["value"], "Expected value property to exist")
353
354 require.NotNil(t, schema.Properties["next"], "Expected next property to exist")
355
356 // The recursive reference should be handled gracefully
357 nextSchema := schema.Properties["next"]
358 require.Equal(t, "object", nextSchema.Type)
359}
360
361func TestGenerateSchemaWithEnumTags(t *testing.T) {
362 t.Parallel()
363
364 type ConfigInput struct {
365 Level string `json:"level" enum:"debug,info,warn,error" description:"Log level"`
366 Format string `json:"format" enum:"json,text"`
367 Optional string `json:"optional,omitempty" enum:"a,b,c"`
368 }
369
370 schema := Generate(reflect.TypeOf(ConfigInput{}))
371
372 // Check level field
373 levelSchema := schema.Properties["level"]
374 require.NotNil(t, levelSchema, "Expected level property to exist")
375 require.Len(t, levelSchema.Enum, 4)
376 expectedLevels := []string{"debug", "info", "warn", "error"}
377 for i, expected := range expectedLevels {
378 require.Equal(t, expected, levelSchema.Enum[i])
379 }
380
381 // Check format field
382 formatSchema := schema.Properties["format"]
383 require.NotNil(t, formatSchema, "Expected format property to exist")
384 require.Len(t, formatSchema.Enum, 2)
385
386 // Check required fields (optional should not be required due to omitempty)
387 expectedRequired := []string{"level", "format"}
388 require.Len(t, schema.Required, len(expectedRequired))
389}
390
391func TestGenerateSchemaComplexTypes(t *testing.T) {
392 t.Parallel()
393
394 type ComplexInput struct {
395 StringSlice []string `json:"string_slice"`
396 IntMap map[string]int `json:"int_map"`
397 NestedSlice []map[string]string `json:"nested_slice"`
398 Interface any `json:"interface"`
399 }
400
401 schema := Generate(reflect.TypeOf(ComplexInput{}))
402
403 // Check string slice
404 stringSliceSchema := schema.Properties["string_slice"]
405 require.NotNil(t, stringSliceSchema, "Expected string_slice property to exist")
406 require.Equal(t, "array", stringSliceSchema.Type)
407 require.Equal(t, "string", stringSliceSchema.Items.Type)
408
409 // Check int map
410 intMapSchema := schema.Properties["int_map"]
411 require.NotNil(t, intMapSchema, "Expected int_map property to exist")
412 require.Equal(t, "object", intMapSchema.Type)
413
414 // Check nested slice
415 nestedSliceSchema := schema.Properties["nested_slice"]
416 require.NotNil(t, nestedSliceSchema, "Expected nested_slice property to exist")
417 require.Equal(t, "array", nestedSliceSchema.Type)
418 require.Equal(t, "object", nestedSliceSchema.Items.Type)
419
420 // Check interface
421 interfaceSchema := schema.Properties["interface"]
422 require.NotNil(t, interfaceSchema, "Expected interface property to exist")
423 require.Equal(t, "object", interfaceSchema.Type)
424}
425
426func TestToSnakeCase(t *testing.T) {
427 t.Parallel()
428
429 tests := []struct {
430 input string
431 expected string
432 }{
433 {"FirstName", "first_name"},
434 {"XMLHttpRequest", "x_m_l_http_request"},
435 {"ID", "i_d"},
436 {"HTTPSProxy", "h_t_t_p_s_proxy"},
437 {"simple", "simple"},
438 {"", ""},
439 {"A", "a"},
440 {"AB", "a_b"},
441 {"CamelCase", "camel_case"},
442 }
443
444 for _, tt := range tests {
445 t.Run(tt.input, func(t *testing.T) {
446 t.Parallel()
447 result := toSnakeCase(tt.input)
448 require.Equal(t, tt.expected, result, "toSnakeCase(%s)", tt.input)
449 })
450 }
451}
452
453func TestSchemaToParametersEdgeCases(t *testing.T) {
454 t.Parallel()
455
456 tests := []struct {
457 name string
458 schema Schema
459 expected map[string]any
460 }{
461 {
462 name: "non-object schema",
463 schema: Schema{
464 Type: "string",
465 },
466 expected: map[string]any{},
467 },
468 {
469 name: "object with no properties",
470 schema: Schema{
471 Type: "object",
472 Properties: nil,
473 },
474 expected: map[string]any{},
475 },
476 {
477 name: "object with empty properties",
478 schema: Schema{
479 Type: "object",
480 Properties: map[string]*Schema{},
481 },
482 expected: map[string]any{},
483 },
484 {
485 name: "schema with all constraint types",
486 schema: Schema{
487 Type: "object",
488 Properties: map[string]*Schema{
489 "text": {
490 Type: "string",
491 Format: "email",
492 MinLength: func() *int { v := 5; return &v }(),
493 MaxLength: func() *int { v := 100; return &v }(),
494 },
495 "number": {
496 Type: "number",
497 Minimum: func() *float64 { v := 0.0; return &v }(),
498 Maximum: func() *float64 { v := 100.0; return &v }(),
499 },
500 },
501 },
502 expected: map[string]any{
503 "text": map[string]any{
504 "type": "string",
505 "format": "email",
506 "minLength": 5,
507 "maxLength": 100,
508 },
509 "number": map[string]any{
510 "type": "number",
511 "minimum": 0.0,
512 "maximum": 100.0,
513 },
514 },
515 },
516 }
517
518 for _, tt := range tests {
519 t.Run(tt.name, func(t *testing.T) {
520 t.Parallel()
521 result := ToParameters(tt.schema)
522 require.Len(t, result, len(tt.expected))
523 for key, expectedValue := range tt.expected {
524 require.NotNil(t, result[key], "Expected parameter %s to exist", key)
525 // Deep comparison would be complex, so we'll check key properties
526 resultParam := result[key].(map[string]any)
527 expectedParam := expectedValue.(map[string]any)
528 for propKey, propValue := range expectedParam {
529 require.Equal(t, propValue, resultParam[propKey], "Expected %s.%s", key, propKey)
530 }
531 }
532 })
533 }
534}