init
This commit is contained in:
278
pkg/util/json_schema/json_schema.go
Normal file
278
pkg/util/json_schema/json_schema.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ForType generates a JSON Schema for the given reflect.Type.
|
||||
// It supports struct fields with json tags and desc tags for metadata.
|
||||
func ForType(t reflect.Type) (map[string]any, error) {
|
||||
definitions := make(map[string]any)
|
||||
schema, err := forTypeInternal(t, "", make(map[reflect.Type]string), definitions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(definitions) == 0 {
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": definitions,
|
||||
}
|
||||
maps.Copy(result, schema)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func forTypeInternal(
|
||||
t reflect.Type,
|
||||
fieldName string,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
) (map[string]any, error) {
|
||||
if t == nil {
|
||||
return nil, errors.New("type cannot be nil")
|
||||
}
|
||||
|
||||
// Dereference pointer types
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// Handle previously visited types
|
||||
if refName, ok := visited[t]; ok {
|
||||
return map[string]any{"$ref": "#/definitions/" + refName}, nil
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
return handleStructType(t, fieldName, visited, definitions)
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
return handleArrayType(t, visited, definitions)
|
||||
|
||||
case reflect.Map:
|
||||
return handleMapType(t, visited, definitions)
|
||||
|
||||
default:
|
||||
return handlePrimitiveType(t)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStructType(
|
||||
t reflect.Type,
|
||||
fieldName string,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
) (map[string]any, error) {
|
||||
// Handle special types.
|
||||
if t == reflect.TypeOf(time.Time{}) {
|
||||
return map[string]any{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if t == reflect.TypeOf(time.Duration(0)) {
|
||||
return map[string]any{
|
||||
"type": "string",
|
||||
"format": "duration",
|
||||
"pattern": "^([0-9]+(s|m|h))+$",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate type name.
|
||||
typeName := t.Name()
|
||||
if typeName == "" {
|
||||
typeName = "Anonymous" + fieldName
|
||||
}
|
||||
visited[t] = typeName
|
||||
|
||||
// Process schema.
|
||||
schema := map[string]any{"type": "object"}
|
||||
|
||||
properties, err := handleStructFields(t, visited, definitions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "handle struct fields")
|
||||
}
|
||||
if len(properties) > 0 {
|
||||
schema["properties"] = properties
|
||||
}
|
||||
|
||||
definitions[typeName] = schema
|
||||
|
||||
return map[string]any{"$ref": "#/definitions/" + typeName}, nil
|
||||
}
|
||||
|
||||
func handleStructFields(
|
||||
t reflect.Type,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
) (properties map[string]any, err error) {
|
||||
properties = make(map[string]any, t.NumField())
|
||||
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
propName := getPropertyName(field)
|
||||
if propName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Anonymous {
|
||||
if err := handleEmbeddedStruct(field, visited, definitions, properties); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
fieldSchema, err := forTypeInternal(field.Type, field.Name, visited, definitions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "generating schema for field %s", field.Name)
|
||||
}
|
||||
|
||||
if desc := field.Tag.Get("desc"); desc != "" {
|
||||
fieldSchema["description"] = desc
|
||||
}
|
||||
|
||||
properties[propName] = fieldSchema
|
||||
}
|
||||
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func handleArrayType(
|
||||
t reflect.Type,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
) (map[string]any, error) {
|
||||
itemSchema, err := forTypeInternal(t.Elem(), "", visited, definitions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "generating array item schema")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "array",
|
||||
"items": itemSchema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleMapType(
|
||||
t reflect.Type,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
) (map[string]any, error) {
|
||||
if t.Key().Kind() != reflect.String {
|
||||
return nil, errors.Errorf("unsupported map key type: %s (must be string)", t.Key().Kind())
|
||||
}
|
||||
|
||||
valueSchema, err := forTypeInternal(t.Elem(), "", visited, definitions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "generating map value schema")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"additionalProperties": valueSchema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handlePrimitiveType(t reflect.Type) (map[string]any, error) {
|
||||
schema := make(map[string]any)
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
schema["type"] = "string"
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if t == reflect.TypeOf(time.Duration(0)) {
|
||||
schema["type"] = "string"
|
||||
schema["format"] = "duration"
|
||||
schema["pattern"] = "^([0-9]+(s|m|h))+$"
|
||||
} else {
|
||||
schema["type"] = "integer"
|
||||
}
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
schema["type"] = "integer"
|
||||
schema["minimum"] = 0
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
schema["type"] = "number"
|
||||
|
||||
case reflect.Bool:
|
||||
schema["type"] = "boolean"
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported type: %s", t.Kind())
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func getPropertyName(field reflect.StructField) string {
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag == "-" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if jsonTag != "" {
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return field.Name
|
||||
}
|
||||
|
||||
func handleEmbeddedStruct(
|
||||
field reflect.StructField,
|
||||
visited map[reflect.Type]string,
|
||||
definitions map[string]any,
|
||||
properties map[string]any,
|
||||
) error {
|
||||
embeddedSchema, err := forTypeInternal(field.Type, "", visited, definitions)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "generating schema for embedded field %s", field.Name)
|
||||
}
|
||||
|
||||
if embeddedType, ok := embeddedSchema["$ref"]; ok {
|
||||
refType := embeddedType.(string)
|
||||
key := strings.TrimPrefix(refType, "#/definitions/")
|
||||
if def, ok := definitions[key]; ok {
|
||||
if embeddedProps, ok := def.(map[string]any)["properties"].(map[string]any); ok {
|
||||
maps.Copy(properties, embeddedProps)
|
||||
}
|
||||
|
||||
delete(definitions, key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user