Ai
1 Star 0 Fork 0

蒙蒙的男孩/eosc

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
schema.go 19.34 KB
一键复制 编辑 原始数据 按行查看 历史
蒙蒙的男孩 提交于 2024-01-10 13:49 +08:00 . 重定义项目地址
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
// Package schema implements OpenAPI 3 compatible JSON Schema which can be
// generated from structs.
package schema
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"gitee.com/meng_mengs_boys/eosc"
)
// ErrSchemaInvalid is sent when there is a problem building the schema.
var ErrSchemaInvalid = errors.New("schema is invalid")
// Mode defines whether the schema is being generated for read or
// write mode. Read-only fields are dropped when in write mode, for example.
type Mode int
type RequireId = eosc.RequireId
type FormatterConfigType = eosc.FormatterConfig
type EoFile = eosc.GzipFile
type EoFileList = eosc.EoFiles
const (
// ModeAll is for general purpose use and includes all fields.
ModeAll Mode = iota
// ModeRead is for HTTP HEAD & GET and will hide write-only fields.
ModeRead
// ModeWrite is for HTTP POST, PUT, PATCH, DELETE and will hide
// read-only fields.
ModeWrite
)
// JSON Schema type constants
const (
TypeBoolean = "boolean"
TypeInteger = "integer"
TypeNumber = "number"
TypeString = "string"
TypeArray = "array"
TypeObject = "object"
TypeMap = "map"
TypeRequireId = "require"
TypeFileList = "eofiles"
TypeFile = "eofile"
TypeFormatter = "formatter"
)
var (
requireType = reflect.TypeOf(RequireId(""))
fileType = reflect.TypeOf(EoFile{})
fileListType = reflect.TypeOf(EoFileList{})
formatterType = reflect.TypeOf(FormatterConfigType{})
timeType = reflect.TypeOf(time.Time{})
ipType = reflect.TypeOf(net.IP{})
uriType = reflect.TypeOf(url.URL{})
)
// I returns a pointer to the given int. Useful helper function for pointer
// schema validators like MaxLength or MinItems.
func I(value uint64) *uint64 {
return &value
}
// F returns a pointer to the given float64. Useful helper function for pointer
// schema validators like Maximum or Minimum.
func F(value float64) *float64 {
return &value
}
// getTagValue returns a value of the schema's type for the given tag string.
// Uses JSON parsing if the schema is not a string.
func getTagValue(s *Schema, t reflect.Type, value string) (interface{}, error) {
// Special case: strings don't need quotes.
if s.Type == TypeString {
return value, nil
}
// Special case: array of strings with comma-separated values and no quotes.
if s.Type == TypeArray && s.Items != nil && s.Items.Type == TypeString && len(value) > 0 && value[0] != '[' {
values := []string{}
for _, s := range strings.Split(value, ",") {
values = append(values, strings.TrimSpace(s))
}
return values, nil
}
var v interface{}
if err := json.Unmarshal([]byte(value), &v); err != nil {
return nil, err
}
vv := reflect.ValueOf(v)
tv := reflect.TypeOf(v)
if v != nil && tv != t {
if tv.Kind() == reflect.Slice {
// Slices can't be cast due to the different layouts. Instead, we make a
// new instance of the destination slice, and convert each value in
// the original to the new type.
tmp := reflect.MakeSlice(t, 0, vv.Len())
for i := 0; i < vv.Len(); i++ {
if !vv.Index(i).Elem().Type().ConvertibleTo(t.Elem()) {
return nil, fmt.Errorf("unable to convert %v to %v: %w", vv.Index(i).Interface(), t.Elem(), ErrSchemaInvalid)
}
tmp = reflect.Append(tmp, vv.Index(i).Elem().Convert(t.Elem()))
}
v = tmp.Interface()
} else if !tv.ConvertibleTo(t) {
return nil, fmt.Errorf("unable to convert %v to %v: %w", tv, t, ErrSchemaInvalid)
}
v = reflect.ValueOf(v).Convert(t).Interface()
}
return v, nil
}
// Schema represents a JSON Schema which can be generated from Go structs
type Schema struct {
//Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
EOType string `json:"eo:type,omitempty"`
Description string `json:"description,omitempty"`
Items *Schema `json:"items,omitempty"`
Properties map[string]*Schema `json:"properties,omitempty"`
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
UISort []string `json:"ui:sort,omitempty"`
Required []string `json:"required,omitempty"`
EmptyLabel string `json:"empty_label,omitempty"`
Format string `json:"format,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
Default interface{} `json:"default,omitempty"`
Example interface{} `json:"example,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
ExclusiveMinimum *bool `json:"exclusiveMinimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMaximum *bool `json:"exclusiveMaximum,omitempty"`
MultipleOf float64 `json:"multipleOf,omitempty"`
MinLength *uint64 `json:"minLength,omitempty"`
MaxLength *uint64 `json:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
MinItems *uint64 `json:"minItems,omitempty"`
MaxItems *uint64 `json:"maxItems,omitempty"`
UniqueItems bool `json:"uniqueItems,omitempty"`
MinProperties *uint64 `json:"minProperties,omitempty"`
MaxProperties *uint64 `json:"maxProperties,omitempty"`
AllOf []*Schema `json:"allOf,omitempty"`
AnyOf []*Schema `json:"anyOf,omitempty"`
OneOf []*Schema `json:"oneOf,omitempty"`
Not *Schema `json:"not,omitempty"`
Nullable bool `json:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Ref string `json:"$ref,omitempty"`
Dependencies map[string][]string `json:"dependencies,omitempty"`
Skill string `json:"skill,omitempty"`
Switch string `json:"switch,omitempty"`
Label string `json:"label,omitempty"`
}
func (s *Schema) findProperties(name string) *Schema {
return s.Properties[name]
}
func (s *Schema) hasProperties(name string) bool {
_, has := s.Properties[name]
return has
}
func (s *Schema) checkDependencies() error {
//判断scheme的dependencies存不存在,存在则校验里面的key及其依赖在properties里存在
if s.EOType == TypeObject && s.Dependencies != nil {
for key, dps := range s.Dependencies {
if !s.hasProperties(key) {
return fmt.Errorf("Create Json Schema Fail: dependencies key:%s is not exist in properties. ", key)
}
for _, dp := range dps {
if !s.hasProperties(dp) {
return fmt.Errorf("Create Json Schema Fail: dependencies key:%s is not exist in properties. ", dp)
}
}
}
}
return nil
}
// HasValidation returns true if at least one validator is set on the schema.
// This excludes the schema's type but includes most other fields and can be
// used to trigger additional slow validation steps when needed.
func (s *Schema) HasValidation() bool {
if s.Items != nil || len(s.Properties) > 0 || len(s.Enum) > 0 || s.Minimum != nil || s.ExclusiveMinimum != nil || s.Maximum != nil || s.ExclusiveMaximum != nil || s.MultipleOf != 0 || s.MinLength != nil || s.MaxLength != nil || s.Pattern != "" || s.MinItems != nil || s.MaxItems != nil || s.UniqueItems || s.MinProperties != nil || s.MaxProperties != nil || len(s.AllOf) > 0 || len(s.AnyOf) > 0 || len(s.OneOf) > 0 || s.Not != nil || s.Ref != "" {
return true
}
return false
}
// Generate creates a JSON schema for a Go type. Struct field tags
// can be used to provide additional metadata such as descriptions and
// validation.
func Generate(t reflect.Type, dependencies map[string][]string) (*Schema, error) {
sc, err := generateWithMode(t, ModeAll, &Schema{Dependencies: dependencies})
if err != nil {
return nil, err
}
err = sc.checkDependencies()
if err != nil {
return nil, err
}
return sc, nil
}
// getFields performs a breadth-first search for all fields including embedded
// ones. It may return multiple fields with the same name, the first of which
// represents the outer-most declaration.
func getFields(typ reflect.Type) []reflect.StructField {
fields := make([]reflect.StructField, 0, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if f.Anonymous {
newTyp := f.Type
if newTyp.Kind() == reflect.Ptr {
newTyp = newTyp.Elem()
}
if newTyp.Kind() == reflect.Struct {
fields = append(fields, getFields(newTyp)...)
}
continue
}
fields = append(fields, f)
}
return fields
}
// generateFromField generates a schema for a single struct field. It returns
// the computed field name, whether it is optional, its schema, and any error
// which may have occurred.
func generateFromField(f reflect.StructField, mode Mode) (name string, required bool, s *Schema, err error) {
jsonTags := strings.Split(f.Tag.Get("json"), ",")
name = strings.ToLower(f.Name)
if len(jsonTags) > 0 && jsonTags[0] != "" {
name = jsonTags[0]
}
if name == "-" {
// Skip deliberately filtered out items
return name, required, nil, nil
}
// 跳过,兼容不导出schema,又需要使用到json标签的结构体字段
if _, ok := f.Tag.Lookup("skip"); ok {
return name, required, nil, nil
}
schema := &Schema{}
//生成field 类型的对应schema
s, err = generateWithMode(f.Type, mode, schema)
if err != nil {
return name, required, nil, err
}
if tag, ok := f.Tag.Lookup("required"); ok {
required = tag != "false"
} else if s.Type == TypeRequireId {
required = true
}
if tag, ok := f.Tag.Lookup("empty_label"); ok {
s.EmptyLabel = tag
}
//找到dependencies并且生成
if tag, ok := f.Tag.Lookup("dependencies"); ok {
attrList := strings.Split(tag, " ")
dependencies := make(map[string][]string)
for _, attrs := range attrList {
idx := strings.Index(attrs, ":")
if idx == -1 {
return name, required, nil, fmt.Errorf("Create Json Schema Fail. StructField %s: dependencies tag format err: %s. ", name, tag)
}
key := attrs[:idx]
dps := strings.Split(attrs[idx+1:], ";")
for _, dp := range dps {
if dp == "" {
return name, required, nil, fmt.Errorf("Create Json Schema Fail. StructField %s: dependencies tag format err: %s. ", name, tag)
}
}
dependencies[key] = dps
}
schema.Dependencies = dependencies
if err = schema.checkDependencies(); err != nil {
return name, required, nil, err
}
}
if tag, ok := f.Tag.Lookup("description"); ok {
s.Description = tag
}
if tag, ok := f.Tag.Lookup("doc"); ok {
s.Description = tag
}
if tag, ok := f.Tag.Lookup("format"); ok {
s.Format = tag
}
if tag, ok := f.Tag.Lookup("enum"); ok {
s.Enum = []interface{}{}
enumType := f.Type
enumSchema := s
if s.Type == TypeArray {
// Enum values should be the type of the array elements, not the
// array itself!
enumType = f.Type.Elem()
enumSchema = s.Items
}
for _, v := range strings.Split(tag, ",") {
parsed, err := getTagValue(enumSchema, enumType, v)
if err != nil {
return name, required, nil, err
}
enumSchema.Enum = append(enumSchema.Enum, parsed)
}
}
if tag, ok := f.Tag.Lookup("default"); ok {
v, err := getTagValue(s, f.Type, tag)
if err != nil {
return name, required, nil, err
}
s.Default = v
}
if tag, ok := f.Tag.Lookup("example"); ok {
v, err := getTagValue(s, f.Type, tag)
if err != nil {
return name, required, nil, err
}
s.Example = v
}
if tag, ok := f.Tag.Lookup("minimum"); ok {
min, err := strconv.ParseFloat(tag, 64)
if err != nil {
return name, required, nil, err
}
s.Minimum = &min
}
if tag, ok := f.Tag.Lookup("exclusiveMinimum"); ok {
min, err := strconv.ParseFloat(tag, 64)
if err != nil {
return name, required, nil, err
}
s.Minimum = &min
t := true
s.ExclusiveMinimum = &t
}
if tag, ok := f.Tag.Lookup("maximum"); ok {
max, err := strconv.ParseFloat(tag, 64)
if err != nil {
return name, required, nil, err
}
s.Maximum = &max
}
if tag, ok := f.Tag.Lookup("exclusiveMaximum"); ok {
max, err := strconv.ParseFloat(tag, 64)
if err != nil {
return name, required, nil, err
}
s.Maximum = &max
t := true
s.ExclusiveMaximum = &t
}
if tag, ok := f.Tag.Lookup("multipleOf"); ok {
mof, err := strconv.ParseFloat(tag, 64)
if err != nil {
return name, required, nil, err
}
s.MultipleOf = mof
}
if tag, ok := f.Tag.Lookup("minLength"); ok {
min, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MinLength = &min
}
if tag, ok := f.Tag.Lookup("maxLength"); ok {
max, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MaxLength = &max
}
if tag, ok := f.Tag.Lookup("pattern"); ok {
s.Pattern = tag
if _, err := regexp.Compile(s.Pattern); err != nil {
return name, required, nil, err
}
}
if tag, ok := f.Tag.Lookup("minItems"); ok {
min, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MinItems = &min
}
if tag, ok := f.Tag.Lookup("maxItems"); ok {
max, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MaxItems = &max
}
if tag, ok := f.Tag.Lookup("uniqueItems"); ok {
if !(tag == "true" || tag == "false") {
return name, required, nil, fmt.Errorf("%s uniqueItems: boolean should be true or false: %w", f.Name, ErrSchemaInvalid)
}
s.UniqueItems = tag == "true"
}
if tag, ok := f.Tag.Lookup("minProperties"); ok {
min, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MinProperties = &min
}
if tag, ok := f.Tag.Lookup("maxProperties"); ok {
max, err := strconv.ParseUint(tag, 10, 64)
if err != nil {
return name, required, nil, err
}
s.MaxProperties = &max
}
if tag, ok := f.Tag.Lookup("nullable"); ok {
if !(tag == "true" || tag == "false") {
return name, required, nil, fmt.Errorf("%s nullable: boolean should be true or false but got %s: %w", f.Name, tag, ErrSchemaInvalid)
}
s.Nullable = tag == "true"
}
if tag, ok := f.Tag.Lookup("readOnly"); ok {
if !(tag == "true" || tag == "false") {
return name, required, nil, fmt.Errorf("%s readOnly: boolean should be true or false: %w", f.Name, ErrSchemaInvalid)
}
s.ReadOnly = tag == "true"
}
if tag, ok := f.Tag.Lookup("writeOnly"); ok {
if !(tag == "true" || tag == "false") {
return name, required, nil, fmt.Errorf("%s writeOnly: boolean should be true or false: %w", f.Name, ErrSchemaInvalid)
}
s.WriteOnly = tag == "true"
}
if tag, ok := f.Tag.Lookup("deprecated"); ok {
if !(tag == "true" || tag == "false") {
return name, required, nil, fmt.Errorf("%s deprecated: boolean should be true or false: %w", f.Name, ErrSchemaInvalid)
}
s.Deprecated = tag == "true"
}
//eosc target skill
if tag, ok := f.Tag.Lookup("skill"); ok {
s.Skill = tag
}
if tag, ok := f.Tag.Lookup("switch"); ok {
s.Switch = tag
}
if tag, ok := f.Tag.Lookup("label"); ok {
s.Label = tag
}
if tag, ok := f.Tag.Lookup("eotype"); ok {
s.EOType = tag
}
return name, required, s, nil
}
// generateWithMode creates a JSON schema for a Go type. Struct field
// tags can be used to provide additional metadata such as descriptions and
// validation. The mode can be all, read, or write. In read or write mode
// any field that is marked as the opposite will be excluded, e.g. a
// write-only field would not be included in read mode. If a schema is given
// as input, add to it, otherwise creates a new schema.
func generateWithMode(t reflect.Type, mode Mode, schema *Schema) (r *Schema, err error) {
if schema == nil {
schema = &Schema{}
}
r = schema
defer func() {
if r != nil && r.EOType == "" {
r.EOType = r.Type
switch r.Type {
case TypeFormatter:
r.Type = TypeObject
r.AdditionalProperties = &Schema{Type: TypeArray, Items: &Schema{Type: TypeString}}
case TypeRequireId:
r.Type = TypeString
case TypeMap:
r.Type = TypeObject
case TypeFile:
r.Type = TypeObject
case TypeFileList:
r.Type = TypeArray
r.Items = &Schema{Type: TypeObject}
}
}
}()
switch t {
case requireType:
{
schema.Type = TypeRequireId
return schema, nil
}
case fileType:
{
schema.Type = TypeFile
return schema, nil
}
case fileListType:
{
schema.Type = TypeFileList
return schema, nil
}
case formatterType:
{
schema.Type = TypeFormatter
return schema, nil
}
case ipType:
{
// Special case: IP address.
schema.Type = TypeString
schema.Format = "ipv4"
return schema, nil
}
}
switch t.Kind() {
case reflect.Struct:
// Handle special cases.
switch t {
case timeType:
schema.Type = TypeString
schema.Format = "date-time"
return schema, nil
case uriType:
schema.Type = TypeString
schema.Format = "uri"
return schema, nil
}
properties := make(map[string]*Schema, 0)
requiredList := make([]string, 0)
uiSort := make([]string, 0)
schema.Type = TypeObject
for _, f := range getFields(t) {
name, required, s, err := generateFromField(f, mode)
if err != nil {
return nil, err
}
if s == nil {
// Skip deliberately filtered out items
continue
}
if s.ReadOnly && mode == ModeWrite {
continue
}
if s.WriteOnly && mode == ModeRead {
continue
}
properties[name] = s
uiSort = append(uiSort, name)
//propertiesSet[name] = struct{}{}
if required {
requiredList = append(requiredList, name)
}
}
if len(properties) > 0 {
schema.UISort = uiSort
schema.Properties = properties
}
if len(requiredList) > 0 {
schema.Required = requiredList
}
case reflect.Map:
schema.Type = TypeMap
s, err := generateWithMode(t.Elem(), mode, nil)
if err != nil {
return nil, err
}
schema.AdditionalProperties = s
case reflect.Slice, reflect.Array:
if t.Elem().Kind() == reflect.Uint8 {
// Special case: `[]byte` should be a BaseConfig-64 string.
schema.Type = TypeString
} else {
schema.Type = TypeArray
s, err := generateWithMode(t.Elem(), mode, nil)
if err != nil {
return nil, err
}
schema.Items = s
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
schema.Type = TypeInteger
schema.Format = "int32"
case reflect.Int64:
schema.Type = TypeInteger
schema.Format = "int64"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
// Unsigned integers can't be negative.
schema.Type = TypeInteger
schema.Format = "int32"
schema.Minimum = F(0.0)
case reflect.Uint64:
schema.Type = TypeInteger
schema.Format = "int64"
schema.Minimum = F(0.0)
case reflect.Float32:
schema.Type = TypeNumber
schema.Format = "float"
case reflect.Float64:
schema.Type = TypeNumber
schema.Format = "double"
case reflect.Bool:
schema.Type = TypeBoolean
case reflect.String:
schema.Type = TypeString
case reflect.Ptr:
return generateWithMode(t.Elem(), mode, schema)
case reflect.Interface:
// Interfaces can be any type.
case reflect.Uintptr, reflect.UnsafePointer, reflect.Func:
// Ignored...
default:
return nil, fmt.Errorf("unsupported type %s from %s", t.Kind(), t)
}
return schema, nil
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/meng_mengs_boys/eosc.git
git@gitee.com:meng_mengs_boys/eosc.git
meng_mengs_boys
eosc
eosc
v1.15.7

搜索帮助