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 }