schema_test.go

  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}