forked from ebhomengo/niki
597 lines
13 KiB
Go
597 lines
13 KiB
Go
package swag
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sv-tools/openapi/spec"
|
|
)
|
|
|
|
type structFieldV3 struct {
|
|
schemaType string
|
|
arrayType string
|
|
formatType string
|
|
maximum *int
|
|
minimum *int
|
|
multipleOf *int
|
|
maxLength *int
|
|
minLength *int
|
|
maxItems *int
|
|
minItems *int
|
|
exampleValue interface{}
|
|
enums []interface{}
|
|
enumVarNames []interface{}
|
|
unique bool
|
|
pattern string
|
|
}
|
|
|
|
func (sf *structFieldV3) 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 *structFieldV3) setMin(valValue string) {
|
|
value, err := strconv.Atoi(valValue)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch sf.schemaType {
|
|
case INTEGER, NUMBER:
|
|
sf.minimum = &value
|
|
case STRING:
|
|
sf.minLength = &value
|
|
case ARRAY:
|
|
sf.minItems = &value
|
|
}
|
|
}
|
|
|
|
func (sf *structFieldV3) setMax(valValue string) {
|
|
value, err := strconv.Atoi(valValue)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch sf.schemaType {
|
|
case INTEGER, NUMBER:
|
|
sf.maximum = &value
|
|
case STRING:
|
|
sf.maxLength = &value
|
|
case ARRAY:
|
|
sf.maxItems = &value
|
|
}
|
|
}
|
|
|
|
type tagBaseFieldParserV3 struct {
|
|
p *Parser
|
|
file *ast.File
|
|
field *ast.Field
|
|
tag reflect.StructTag
|
|
}
|
|
|
|
func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 {
|
|
fieldParser := tagBaseFieldParserV3{
|
|
p: p,
|
|
file: file,
|
|
field: field,
|
|
tag: "",
|
|
}
|
|
if fieldParser.field.Tag != nil {
|
|
fieldParser.tag = reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", ""))
|
|
}
|
|
|
|
return &fieldParser
|
|
}
|
|
|
|
func (ps *tagBaseFieldParserV3) CustomSchema() (*spec.RefOrSpec[spec.Schema], error) {
|
|
if ps.field.Tag == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
typeTag := ps.tag.Get(swaggerTypeTag)
|
|
if typeTag != "" {
|
|
return BuildCustomSchemaV3(strings.Split(typeTag, ","))
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ComplementSchema complement schema with field properties
|
|
func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error {
|
|
if schema.Spec == nil {
|
|
schema = ps.p.openAPI.Components.Spec.Schemas[strings.ReplaceAll(schema.Ref.Ref, "#/components/schemas/", "")]
|
|
if schema == nil {
|
|
return fmt.Errorf("could not resolve schema for ref %s", schema.Ref.Ref)
|
|
}
|
|
}
|
|
|
|
types := ps.p.GetSchemaTypePathV3(schema, 2)
|
|
if len(types) == 0 {
|
|
return fmt.Errorf("invalid type for field: %s", ps.field.Names[0])
|
|
}
|
|
|
|
if schema.Ref != nil { //IsRefSchema(schema)
|
|
// TODO fetch existing schema from components
|
|
var newSchema = spec.Schema{}
|
|
err := ps.complementSchema(&newSchema, types)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !reflect.ValueOf(newSchema).IsZero() {
|
|
newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}}
|
|
*schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return ps.complementSchema(schema.Spec, types)
|
|
}
|
|
|
|
// complementSchema complement schema with field properties
|
|
func (ps *tagBaseFieldParserV3) 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 := &structFieldV3{
|
|
schemaType: types[0],
|
|
formatType: ps.tag.Get(formatTag),
|
|
}
|
|
|
|
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 != "" {
|
|
field.parseValidTags(bindingTagValue)
|
|
}
|
|
|
|
validateTagValue := ps.tag.Get(validateTag)
|
|
if validateTagValue != "" {
|
|
field.parseValidTags(validateTagValue)
|
|
}
|
|
|
|
enumsTagValue := ps.tag.Get(enumsTag)
|
|
if enumsTagValue != "" {
|
|
err := field.parseEnumTags(enumsTagValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if IsNumericType(field.schemaType) || IsNumericType(field.arrayType) {
|
|
maximum, err := getIntTagV3(ps.tag, maximumTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if maximum != nil {
|
|
field.maximum = maximum
|
|
}
|
|
|
|
minimum, err := getIntTagV3(ps.tag, minimumTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if minimum != nil {
|
|
field.minimum = minimum
|
|
}
|
|
|
|
multipleOf, err := getIntTagV3(ps.tag, multipleOfTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if multipleOf != nil {
|
|
field.multipleOf = multipleOf
|
|
}
|
|
}
|
|
|
|
if field.schemaType == STRING || field.arrayType == STRING {
|
|
maxLength, err := getIntTagV3(ps.tag, maxLengthTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if maxLength != nil {
|
|
field.maxLength = maxLength
|
|
}
|
|
|
|
minLength, err := getIntTagV3(ps.tag, minLengthTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if minLength != nil {
|
|
field.minLength = minLength
|
|
}
|
|
|
|
pattern, ok := ps.tag.Lookup(patternTag)
|
|
if ok {
|
|
field.pattern = pattern
|
|
}
|
|
}
|
|
|
|
// 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 = *PrimitiveSchemaV3(field.schemaType).Spec
|
|
|
|
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
|
|
}
|
|
|
|
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.Spec.Extensions == nil {
|
|
schema.Items.Schema.Spec.Extensions = map[string]interface{}{}
|
|
}
|
|
schema.Items.Schema.Spec.Extensions[enumVarNamesExtension] = field.enumVarNames
|
|
} else {
|
|
// Add to top level schema
|
|
if schema.Extensions == nil {
|
|
schema.Extensions = map[string]interface{}{}
|
|
}
|
|
schema.Extensions[enumVarNamesExtension] = field.enumVarNames
|
|
}
|
|
}
|
|
|
|
var oneOfSchemas []*spec.RefOrSpec[spec.Schema]
|
|
oneOfTagValue := ps.tag.Get(oneOfTag)
|
|
if oneOfTagValue != "" {
|
|
oneOfTypes := strings.Split((oneOfTagValue), ",")
|
|
for _, oneOfType := range oneOfTypes {
|
|
oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true)
|
|
if err != nil {
|
|
return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err)
|
|
}
|
|
oneOfSchemas = append(oneOfSchemas, oneOfSchema)
|
|
}
|
|
}
|
|
|
|
elemSchema := schema
|
|
|
|
if field.schemaType == ARRAY {
|
|
// For Array only
|
|
schema.MaxItems = field.maxItems
|
|
schema.MinItems = field.minItems
|
|
schema.UniqueItems = &field.unique
|
|
|
|
elemSchema = schema.Items.Schema.Spec
|
|
if elemSchema == nil {
|
|
elemSchema = ps.p.getSchemaByRef(schema.Items.Schema.Ref)
|
|
}
|
|
|
|
elemSchema.Format = field.formatType
|
|
}
|
|
|
|
elemSchema.Maximum = field.maximum
|
|
elemSchema.Minimum = field.minimum
|
|
elemSchema.MultipleOf = field.multipleOf
|
|
elemSchema.MaxLength = field.maxLength
|
|
elemSchema.MinLength = field.minLength
|
|
elemSchema.Enum = field.enums
|
|
elemSchema.Pattern = field.pattern
|
|
elemSchema.OneOf = oneOfSchemas
|
|
|
|
return nil
|
|
}
|
|
|
|
func getIntTagV3(structTag reflect.StructTag, tagName string) (*int, error) {
|
|
strValue := structTag.Get(tagName)
|
|
if strValue == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
value, err := strconv.Atoi(strValue)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err)
|
|
}
|
|
|
|
return &value, nil
|
|
}
|
|
|
|
func parseValidTagsV3(validTag string, sf *structFieldV3) {
|
|
|
|
// `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":
|
|
if strings.Contains(validTag, "swaggerIgnore") {
|
|
continue
|
|
}
|
|
|
|
sf.setOneOf(valValue)
|
|
case "unique":
|
|
if sf.schemaType == ARRAY {
|
|
sf.unique = true
|
|
}
|
|
case "dive":
|
|
// ignore dive
|
|
return
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (sf *structFieldV3) parseValidTags(validTag string) {
|
|
|
|
// `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":
|
|
if strings.Contains(validTag, "swaggerIgnore") {
|
|
continue
|
|
}
|
|
|
|
sf.setOneOf(valValue)
|
|
case "unique":
|
|
if sf.schemaType == ARRAY {
|
|
sf.unique = true
|
|
}
|
|
case "dive":
|
|
// ignore dive
|
|
return
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (sf *structFieldV3) parseEnumTags(enumTag string) error {
|
|
enumType := sf.schemaType
|
|
if sf.schemaType == ARRAY {
|
|
enumType = sf.arrayType
|
|
}
|
|
|
|
sf.enums = nil
|
|
|
|
for _, e := range strings.Split(enumTag, ",") {
|
|
value, err := defineType(enumType, e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sf.enums = append(sf.enums, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ps *tagBaseFieldParserV3) 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 *tagBaseFieldParserV3) FieldName() (string, error) {
|
|
var name string
|
|
|
|
if ps.field.Tag != nil {
|
|
// json:"tag,hoge"
|
|
name = strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0])
|
|
if name != "" {
|
|
return name, nil
|
|
}
|
|
|
|
// use "form" tag over json tag
|
|
name = ps.FormName()
|
|
if name != "" {
|
|
return name, nil
|
|
}
|
|
}
|
|
|
|
if ps.field.Names == nil {
|
|
return "", nil
|
|
}
|
|
|
|
switch ps.p.PropNamingStrategy {
|
|
case SnakeCase:
|
|
return toSnakeCase(ps.field.Names[0].Name), nil
|
|
case PascalCase:
|
|
return ps.field.Names[0].Name, nil
|
|
default:
|
|
return toLowerCamelCase(ps.field.Names[0].Name), nil
|
|
}
|
|
}
|
|
|
|
func (ps *tagBaseFieldParserV3) FormName() string {
|
|
if ps.field.Tag != nil {
|
|
return strings.TrimSpace(strings.Split(ps.tag.Get(formTag), ",")[0])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (ps *tagBaseFieldParserV3) 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
|
|
}
|