feat: adjust fiber route generation
This commit is contained in:
686
pkg/swag/field_parser.go
Normal file
686
pkg/swag/field_parser.go
Normal file
@@ -0,0 +1,686 @@
|
||||
package swag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
)
|
||||
|
||||
var _ FieldParser = &tagBaseFieldParser{p: nil, field: nil, tag: ""}
|
||||
|
||||
const (
|
||||
requiredLabel = "required"
|
||||
optionalLabel = "optional"
|
||||
swaggerTypeTag = "swaggertype"
|
||||
swaggerIgnoreTag = "swaggerignore"
|
||||
)
|
||||
|
||||
type tagBaseFieldParser struct {
|
||||
p *Parser
|
||||
field *ast.Field
|
||||
tag reflect.StructTag
|
||||
}
|
||||
|
||||
func newTagBaseFieldParser(p *Parser, field *ast.Field) FieldParser {
|
||||
fieldParser := tagBaseFieldParser{
|
||||
p: p,
|
||||
field: field,
|
||||
tag: "",
|
||||
}
|
||||
if fieldParser.field.Tag != nil {
|
||||
fieldParser.tag = reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", ""))
|
||||
}
|
||||
|
||||
return &fieldParser
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) ShouldSkip() bool {
|
||||
// Skip non-exported fields.
|
||||
if ps.field.Names != nil && !ast.IsExported(ps.field.Names[0].Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ps.field.Tag == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ignoreTag := ps.tag.Get(swaggerIgnoreTag)
|
||||
if strings.EqualFold(ignoreTag, "true") {
|
||||
return true
|
||||
}
|
||||
|
||||
// json:"tag,hoge"
|
||||
name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0])
|
||||
if name == "-" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) FieldNames() ([]string, error) {
|
||||
if len(ps.field.Names) <= 1 {
|
||||
// if embedded but with a json/form name ??
|
||||
if ps.field.Tag != nil {
|
||||
// json:"tag,hoge"
|
||||
name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0])
|
||||
if name != "" {
|
||||
return []string{name}, nil
|
||||
}
|
||||
|
||||
// use "form" tag over json tag
|
||||
name = ps.FormName()
|
||||
if name != "" {
|
||||
return []string{name}, nil
|
||||
}
|
||||
}
|
||||
if len(ps.field.Names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
var names = make([]string, 0, len(ps.field.Names))
|
||||
for _, name := range ps.field.Names {
|
||||
switch ps.p.PropNamingStrategy {
|
||||
case SnakeCase:
|
||||
names = append(names, toSnakeCase(name.Name))
|
||||
case PascalCase:
|
||||
names = append(names, name.Name)
|
||||
default:
|
||||
names = append(names, toLowerCamelCase(name.Name))
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) firstTagValue(tag string) string {
|
||||
if ps.field.Tag != nil {
|
||||
return strings.TrimRight(strings.TrimSpace(strings.Split(ps.tag.Get(tag), ",")[0]), "[]")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) FormName() string {
|
||||
return ps.firstTagValue(formTag)
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) HeaderName() string {
|
||||
return ps.firstTagValue(headerTag)
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) PathName() string {
|
||||
return ps.firstTagValue(uriTag)
|
||||
}
|
||||
|
||||
func toSnakeCase(in string) string {
|
||||
var (
|
||||
runes = []rune(in)
|
||||
length = len(runes)
|
||||
out []rune
|
||||
)
|
||||
|
||||
for idx := 0; idx < length; idx++ {
|
||||
if idx > 0 && unicode.IsUpper(runes[idx]) &&
|
||||
((idx+1 < length && unicode.IsLower(runes[idx+1])) || unicode.IsLower(runes[idx-1])) {
|
||||
out = append(out, '_')
|
||||
}
|
||||
|
||||
out = append(out, unicode.ToLower(runes[idx]))
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func toLowerCamelCase(in string) string {
|
||||
var flag bool
|
||||
|
||||
out := make([]rune, len(in))
|
||||
|
||||
runes := []rune(in)
|
||||
for i, curr := range runes {
|
||||
if (i == 0 && unicode.IsUpper(curr)) || (flag && unicode.IsUpper(curr)) {
|
||||
out[i] = unicode.ToLower(curr)
|
||||
flag = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
out[i] = curr
|
||||
flag = false
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) CustomSchema() (*spec.Schema, error) {
|
||||
if ps.field.Tag == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
typeTag := ps.tag.Get(swaggerTypeTag)
|
||||
if typeTag != "" {
|
||||
return BuildCustomSchema(strings.Split(typeTag, ","))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type structField struct {
|
||||
title string
|
||||
schemaType string
|
||||
arrayType string
|
||||
formatType string
|
||||
maximum *float64
|
||||
minimum *float64
|
||||
multipleOf *float64
|
||||
maxLength *int64
|
||||
minLength *int64
|
||||
maxItems *int64
|
||||
minItems *int64
|
||||
exampleValue interface{}
|
||||
enums []interface{}
|
||||
enumVarNames []interface{}
|
||||
unique bool
|
||||
}
|
||||
|
||||
// splitNotWrapped slices s into all substrings separated by sep if sep is not
|
||||
// wrapped by brackets and returns a slice of the substrings between those separators.
|
||||
func splitNotWrapped(s string, sep rune) []string {
|
||||
openCloseMap := map[rune]rune{
|
||||
'(': ')',
|
||||
'[': ']',
|
||||
'{': '}',
|
||||
}
|
||||
|
||||
var (
|
||||
result = make([]string, 0)
|
||||
current = strings.Builder{}
|
||||
openCount = 0
|
||||
openChar rune
|
||||
)
|
||||
|
||||
for _, char := range s {
|
||||
switch {
|
||||
case openChar == 0 && openCloseMap[char] != 0:
|
||||
openChar = char
|
||||
|
||||
openCount++
|
||||
|
||||
current.WriteRune(char)
|
||||
case char == openChar:
|
||||
openCount++
|
||||
|
||||
current.WriteRune(char)
|
||||
case openCount > 0 && char == openCloseMap[openChar]:
|
||||
openCount--
|
||||
|
||||
current.WriteRune(char)
|
||||
case openCount == 0 && char == sep:
|
||||
result = append(result, current.String())
|
||||
|
||||
openChar = 0
|
||||
|
||||
current = strings.Builder{}
|
||||
default:
|
||||
current.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
if current.String() != "" {
|
||||
result = append(result, current.String())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ComplementSchema complement schema with field properties
|
||||
func (ps *tagBaseFieldParser) ComplementSchema(schema *spec.Schema) error {
|
||||
types := ps.p.GetSchemaTypePath(schema, 2)
|
||||
if len(types) == 0 {
|
||||
return fmt.Errorf("invalid type for field: %s", ps.field.Names[0])
|
||||
}
|
||||
|
||||
if IsRefSchema(schema) {
|
||||
var newSchema = spec.Schema{}
|
||||
err := ps.complementSchema(&newSchema, types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.ValueOf(newSchema).IsZero() {
|
||||
*schema = *(newSchema.WithAllOf(*schema))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return ps.complementSchema(schema, types)
|
||||
}
|
||||
|
||||
// complementSchema complement schema with field properties
|
||||
func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []string) error {
|
||||
if ps.field.Tag == nil {
|
||||
if ps.field.Doc != nil {
|
||||
schema.Description = strings.TrimSpace(ps.field.Doc.Text())
|
||||
}
|
||||
|
||||
if schema.Description == "" && ps.field.Comment != nil {
|
||||
schema.Description = strings.TrimSpace(ps.field.Comment.Text())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
field := &structField{
|
||||
schemaType: types[0],
|
||||
formatType: ps.tag.Get(formatTag),
|
||||
title: ps.tag.Get(titleTag),
|
||||
}
|
||||
|
||||
if len(types) > 1 && (types[0] == ARRAY || types[0] == OBJECT) {
|
||||
field.arrayType = types[1]
|
||||
}
|
||||
|
||||
jsonTagValue := ps.tag.Get(jsonTag)
|
||||
|
||||
bindingTagValue := ps.tag.Get(bindingTag)
|
||||
if bindingTagValue != "" {
|
||||
parseValidTags(bindingTagValue, field)
|
||||
}
|
||||
|
||||
validateTagValue := ps.tag.Get(validateTag)
|
||||
if validateTagValue != "" {
|
||||
parseValidTags(validateTagValue, field)
|
||||
}
|
||||
|
||||
enumsTagValue := ps.tag.Get(enumsTag)
|
||||
if enumsTagValue != "" {
|
||||
err := parseEnumTags(enumsTagValue, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if IsNumericType(field.schemaType) || IsNumericType(field.arrayType) {
|
||||
maximum, err := getFloatTag(ps.tag, maximumTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if maximum != nil {
|
||||
field.maximum = maximum
|
||||
}
|
||||
|
||||
minimum, err := getFloatTag(ps.tag, minimumTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if minimum != nil {
|
||||
field.minimum = minimum
|
||||
}
|
||||
|
||||
multipleOf, err := getFloatTag(ps.tag, multipleOfTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if multipleOf != nil {
|
||||
field.multipleOf = multipleOf
|
||||
}
|
||||
}
|
||||
|
||||
if field.schemaType == STRING || field.arrayType == STRING {
|
||||
maxLength, err := getIntTag(ps.tag, maxLengthTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if maxLength != nil {
|
||||
field.maxLength = maxLength
|
||||
}
|
||||
|
||||
minLength, err := getIntTag(ps.tag, minLengthTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if minLength != nil {
|
||||
field.minLength = minLength
|
||||
}
|
||||
}
|
||||
|
||||
// json:"name,string" or json:",string"
|
||||
exampleTagValue, ok := ps.tag.Lookup(exampleTag)
|
||||
if ok {
|
||||
field.exampleValue = exampleTagValue
|
||||
|
||||
if !strings.Contains(jsonTagValue, ",string") {
|
||||
example, err := defineTypeOfExample(field.schemaType, field.arrayType, exampleTagValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
field.exampleValue = example
|
||||
}
|
||||
}
|
||||
|
||||
// perform this after setting everything else (min, max, etc...)
|
||||
if strings.Contains(jsonTagValue, ",string") {
|
||||
// @encoding/json: "It applies only to fields of string, floating point, integer, or boolean types."
|
||||
defaultValues := map[string]string{
|
||||
// Zero Values as string
|
||||
STRING: "",
|
||||
INTEGER: "0",
|
||||
BOOLEAN: "false",
|
||||
NUMBER: "0",
|
||||
}
|
||||
|
||||
defaultValue, ok := defaultValues[field.schemaType]
|
||||
if ok {
|
||||
field.schemaType = STRING
|
||||
*schema = *PrimitiveSchema(field.schemaType)
|
||||
|
||||
if field.exampleValue == nil {
|
||||
// if exampleValue is not defined by the user,
|
||||
// we will force an example with a correct value
|
||||
// (eg: int->"0", bool:"false")
|
||||
field.exampleValue = defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ps.field.Doc != nil {
|
||||
schema.Description = strings.TrimSpace(ps.field.Doc.Text())
|
||||
}
|
||||
|
||||
if schema.Description == "" && ps.field.Comment != nil {
|
||||
schema.Description = strings.TrimSpace(ps.field.Comment.Text())
|
||||
}
|
||||
|
||||
schema.ReadOnly = ps.tag.Get(readOnlyTag) == "true"
|
||||
|
||||
defaultTagValue := ps.tag.Get(defaultTag)
|
||||
if defaultTagValue != "" {
|
||||
value, err := defineType(field.schemaType, defaultTagValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schema.Default = value
|
||||
}
|
||||
|
||||
schema.Example = field.exampleValue
|
||||
|
||||
if field.schemaType != ARRAY {
|
||||
schema.Format = field.formatType
|
||||
}
|
||||
schema.Title = field.title
|
||||
|
||||
extensionsTagValue := ps.tag.Get(extensionsTag)
|
||||
if extensionsTagValue != "" {
|
||||
schema.Extensions = setExtensionParam(extensionsTagValue)
|
||||
}
|
||||
|
||||
varNamesTag := ps.tag.Get("x-enum-varnames")
|
||||
if varNamesTag != "" {
|
||||
varNames := strings.Split(varNamesTag, ",")
|
||||
if len(varNames) != len(field.enums) {
|
||||
return fmt.Errorf("invalid count of x-enum-varnames. expected %d, got %d", len(field.enums), len(varNames))
|
||||
}
|
||||
|
||||
field.enumVarNames = nil
|
||||
|
||||
for _, v := range varNames {
|
||||
field.enumVarNames = append(field.enumVarNames, v)
|
||||
}
|
||||
|
||||
if field.schemaType == ARRAY {
|
||||
// Add the var names in the items schema
|
||||
if schema.Items.Schema.Extensions == nil {
|
||||
schema.Items.Schema.Extensions = map[string]interface{}{}
|
||||
}
|
||||
schema.Items.Schema.Extensions[enumVarNamesExtension] = field.enumVarNames
|
||||
} else {
|
||||
// Add to top level schema
|
||||
if schema.Extensions == nil {
|
||||
schema.Extensions = map[string]interface{}{}
|
||||
}
|
||||
schema.Extensions[enumVarNamesExtension] = field.enumVarNames
|
||||
}
|
||||
}
|
||||
|
||||
eleSchema := schema
|
||||
|
||||
if field.schemaType == ARRAY {
|
||||
// For Array only
|
||||
schema.MaxItems = field.maxItems
|
||||
schema.MinItems = field.minItems
|
||||
schema.UniqueItems = field.unique
|
||||
|
||||
eleSchema = schema.Items.Schema
|
||||
eleSchema.Format = field.formatType
|
||||
}
|
||||
|
||||
eleSchema.Maximum = field.maximum
|
||||
eleSchema.Minimum = field.minimum
|
||||
eleSchema.MultipleOf = field.multipleOf
|
||||
eleSchema.MaxLength = field.maxLength
|
||||
eleSchema.MinLength = field.minLength
|
||||
eleSchema.Enum = field.enums
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFloatTag(structTag reflect.StructTag, tagName string) (*float64, error) {
|
||||
strValue := structTag.Get(tagName)
|
||||
if strValue == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(strValue, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err)
|
||||
}
|
||||
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
func getIntTag(structTag reflect.StructTag, tagName string) (*int64, error) {
|
||||
strValue := structTag.Get(tagName)
|
||||
if strValue == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(strValue, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err)
|
||||
}
|
||||
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
func (ps *tagBaseFieldParser) IsRequired() (bool, error) {
|
||||
if ps.field.Tag == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
bindingTag := ps.tag.Get(bindingTag)
|
||||
if bindingTag != "" {
|
||||
for _, val := range strings.Split(bindingTag, ",") {
|
||||
switch val {
|
||||
case requiredLabel:
|
||||
return true, nil
|
||||
case optionalLabel:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateTag := ps.tag.Get(validateTag)
|
||||
if validateTag != "" {
|
||||
for _, val := range strings.Split(validateTag, ",") {
|
||||
switch val {
|
||||
case requiredLabel:
|
||||
return true, nil
|
||||
case optionalLabel:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ps.p.RequiredByDefault, nil
|
||||
}
|
||||
|
||||
func parseValidTags(validTag string, sf *structField) {
|
||||
// `validate:"required,max=10,min=1"`
|
||||
// ps. required checked by IsRequired().
|
||||
for _, val := range strings.Split(validTag, ",") {
|
||||
var (
|
||||
valValue string
|
||||
keyVal = strings.Split(val, "=")
|
||||
)
|
||||
|
||||
switch len(keyVal) {
|
||||
case 1:
|
||||
case 2:
|
||||
valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
switch keyVal[0] {
|
||||
case "max", "lte":
|
||||
sf.setMax(valValue)
|
||||
case "min", "gte":
|
||||
sf.setMin(valValue)
|
||||
case "oneof":
|
||||
sf.setOneOf(valValue)
|
||||
case "unique":
|
||||
if sf.schemaType == ARRAY {
|
||||
sf.unique = true
|
||||
}
|
||||
case "dive":
|
||||
// ignore dive
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnumTags(enumTag string, field *structField) error {
|
||||
enumType := field.schemaType
|
||||
if field.schemaType == ARRAY {
|
||||
enumType = field.arrayType
|
||||
}
|
||||
|
||||
field.enums = nil
|
||||
|
||||
for _, e := range strings.Split(enumTag, ",") {
|
||||
value, err := defineType(enumType, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
field.enums = append(field.enums, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sf *structField) setOneOf(valValue string) {
|
||||
if len(sf.enums) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
enumType := sf.schemaType
|
||||
if sf.schemaType == ARRAY {
|
||||
enumType = sf.arrayType
|
||||
}
|
||||
|
||||
valValues := parseOneOfParam2(valValue)
|
||||
for i := range valValues {
|
||||
value, err := defineType(enumType, valValues[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sf.enums = append(sf.enums, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (sf *structField) setMin(valValue string) {
|
||||
value, err := strconv.ParseFloat(valValue, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch sf.schemaType {
|
||||
case INTEGER, NUMBER:
|
||||
sf.minimum = &value
|
||||
case STRING:
|
||||
intValue := int64(value)
|
||||
sf.minLength = &intValue
|
||||
case ARRAY:
|
||||
intValue := int64(value)
|
||||
sf.minItems = &intValue
|
||||
}
|
||||
}
|
||||
|
||||
func (sf *structField) setMax(valValue string) {
|
||||
value, err := strconv.ParseFloat(valValue, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch sf.schemaType {
|
||||
case INTEGER, NUMBER:
|
||||
sf.maximum = &value
|
||||
case STRING:
|
||||
intValue := int64(value)
|
||||
sf.maxLength = &intValue
|
||||
case ARRAY:
|
||||
intValue := int64(value)
|
||||
sf.maxItems = &intValue
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
utf8HexComma = "0x2C"
|
||||
utf8Pipe = "0x7C"
|
||||
)
|
||||
|
||||
// These code copy from
|
||||
// https://github.com/go-playground/validator/blob/d4271985b44b735c6f76abc7a06532ee997f9476/baked_in.go#L207
|
||||
// ---.
|
||||
var oneofValsCache = map[string][]string{}
|
||||
var oneofValsCacheRWLock = sync.RWMutex{}
|
||||
var splitParamsRegex = regexp.MustCompile(`'[^']*'|\S+`)
|
||||
|
||||
func parseOneOfParam2(param string) []string {
|
||||
oneofValsCacheRWLock.RLock()
|
||||
values, ok := oneofValsCache[param]
|
||||
oneofValsCacheRWLock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
oneofValsCacheRWLock.Lock()
|
||||
values = splitParamsRegex.FindAllString(param, -1)
|
||||
|
||||
for i := 0; i < len(values); i++ {
|
||||
values[i] = strings.ReplaceAll(values[i], "'", "")
|
||||
}
|
||||
|
||||
oneofValsCache[param] = values
|
||||
|
||||
oneofValsCacheRWLock.Unlock()
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// ---.
|
||||
Reference in New Issue
Block a user