// Copyright (C) 2025 wangyusong // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package jsonschema import ( "reflect" "testing" "time" . "github.com/onsi/gomega" "github.com/glidea/zenfeed/pkg/test" ) func TestForType(t *testing.T) { RegisterTestingT(t) type givenDetail struct{} type whenDetail struct { inputType reflect.Type } type thenExpected struct { schema map[string]any hasError bool errorText string } type SimpleStruct struct { Name string `json:"name" desc:"The name field"` Age int `json:"age"` IsActive bool `json:"is_active"` IgnoreField string `json:"-"` } type EmbeddedStruct struct { ID string `json:"id"` } type ComplexStruct struct { EmbeddedStruct Time time.Time `json:"time"` Duration time.Duration `json:"duration"` Tags []string `json:"tags"` Metadata map[string]string `json:"metadata"` } type Node struct { Value string `json:"value"` Next *Node `json:"next"` Children []Node `json:"children"` } type LinkedList struct { Head *Node `json:"head"` } tests := []test.Case[givenDetail, whenDetail, thenExpected]{ { Scenario: "Generate schema for simple struct", When: "providing a struct with basic types", Then: "should generate correct JSON schema", WhenDetail: whenDetail{ inputType: reflect.TypeOf(SimpleStruct{}), }, ThenExpected: thenExpected{ schema: map[string]any{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": map[string]any{ "SimpleStruct": map[string]any{ "type": "object", "properties": map[string]any{ "name": map[string]any{ "type": "string", "description": "The name field", }, "age": map[string]any{ "type": "integer", }, "is_active": map[string]any{ "type": "boolean", }, }, }, }, "$ref": "#/definitions/SimpleStruct", }, }, }, { Scenario: "Generate schema for complex struct", When: "providing a struct with embedded fields and special types", Then: "should generate correct JSON schema with all fields", WhenDetail: whenDetail{ inputType: reflect.TypeOf(ComplexStruct{}), }, ThenExpected: thenExpected{ schema: map[string]any{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": map[string]any{ "ComplexStruct": map[string]any{ "type": "object", "properties": map[string]any{ "id": map[string]any{ "type": "string", }, "time": map[string]any{ "type": "string", "format": "date-time", }, "duration": map[string]any{ "type": "string", "format": "duration", "pattern": "^([0-9]+(s|m|h))+$", }, "tags": map[string]any{ "type": "array", "items": map[string]any{ "type": "string", }, }, "metadata": map[string]any{ "type": "object", "additionalProperties": map[string]any{ "type": "string", }, }, }, }, }, "$ref": "#/definitions/ComplexStruct", }, }, }, { Scenario: "Generate schema for struct with circular reference", When: "providing a struct that references itself", Then: "should generate correct JSON schema using $ref", WhenDetail: whenDetail{ inputType: reflect.TypeOf(Node{}), }, ThenExpected: thenExpected{ schema: map[string]any{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": map[string]any{ "Node": map[string]any{ "type": "object", "properties": map[string]any{ "value": map[string]any{ "type": "string", }, "next": map[string]any{ "$ref": "#/definitions/Node", }, "children": map[string]any{ "type": "array", "items": map[string]any{ "$ref": "#/definitions/Node", }, }, }, }, }, "$ref": "#/definitions/Node", }, }, }, { Scenario: "Generate schema for struct with nested circular reference", When: "providing a struct that contains a circular reference", Then: "should generate correct JSON schema using $ref", WhenDetail: whenDetail{ inputType: reflect.TypeOf(LinkedList{}), }, ThenExpected: thenExpected{ schema: map[string]any{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": map[string]any{ "LinkedList": map[string]any{ "type": "object", "properties": map[string]any{ "head": map[string]any{ "$ref": "#/definitions/Node", }, }, }, "Node": map[string]any{ "type": "object", "properties": map[string]any{ "value": map[string]any{ "type": "string", }, "next": map[string]any{ "$ref": "#/definitions/Node", }, "children": map[string]any{ "type": "array", "items": map[string]any{ "$ref": "#/definitions/Node", }, }, }, }, }, "$ref": "#/definitions/LinkedList", }, }, }, { Scenario: "Generate schema for nil type", When: "providing a nil type", Then: "should return error", WhenDetail: whenDetail{ inputType: nil, }, ThenExpected: thenExpected{ hasError: true, errorText: "type cannot be nil", }, }, { Scenario: "Generate schema for unsupported map key type", When: "providing a map with non-string key type", Then: "should return error", WhenDetail: whenDetail{ inputType: reflect.TypeOf(map[int]string{}), }, ThenExpected: thenExpected{ hasError: true, errorText: "unsupported map key type: int (must be string)", }, }, } for _, tt := range tests { t.Run(tt.Scenario, func(t *testing.T) { // When. schema, err := ForType(tt.WhenDetail.inputType) // Then. if tt.ThenExpected.hasError { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.errorText)) } else { Expect(err).NotTo(HaveOccurred()) Expect(schema).To(Equal(tt.ThenExpected.schema)) } }) } }