github.com/unionj-cloud/go-doudou/v2@v2.3.5/toolkit/openapi/v3/helper.go (about)

     1  package v3
     2  
     3  import (
     4  	"encoding/json"
     5  	"github.com/getkin/kin-openapi/openapi2"
     6  	"github.com/getkin/kin-openapi/openapi2conv"
     7  	"github.com/getkin/kin-openapi/openapi3"
     8  	"github.com/go-resty/resty/v2"
     9  	"github.com/unionj-cloud/go-doudou/v2/toolkit/astutils"
    10  	"github.com/unionj-cloud/go-doudou/v2/toolkit/copier"
    11  	"github.com/unionj-cloud/go-doudou/v2/toolkit/sliceutils"
    12  	"github.com/unionj-cloud/go-doudou/v2/toolkit/stringutils"
    13  	"io/ioutil"
    14  	"os"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strings"
    18  	"unicode"
    19  )
    20  
    21  // Schemas from components of OpenAPI3.0 json document
    22  var Schemas = make(map[string]Schema)
    23  
    24  var Enums = make(map[string]astutils.EnumMeta)
    25  
    26  // SchemaNames schema names from components of OpenAPI3.0 json document
    27  // also struct names from vo package
    28  var SchemaNames []string
    29  
    30  // SchemaOf reference https://golang.org/pkg/builtin/
    31  // type bool
    32  // type byte
    33  // type complex128
    34  // type complex64
    35  // type error
    36  // type float32
    37  // type float64
    38  // type int
    39  // type int16
    40  // type int32
    41  // type int64
    42  // type int8
    43  // type rune
    44  // type string
    45  // type uint
    46  // type uint16
    47  // type uint32
    48  // type uint64
    49  // type uint8
    50  // type uintptr
    51  func SchemaOf(field astutils.FieldMeta) *Schema {
    52  	ft := field.Type
    53  	if IsVarargs(ft) {
    54  		ft = ToSlice(ft)
    55  	}
    56  	ft = strings.TrimLeft(ft, "*")
    57  	switch ft {
    58  	case "int", "int8", "int16", "int32", "uint", "uint8", "uint16", "uint32", "byte", "rune", "complex64", "complex128":
    59  		return Int
    60  	case "int64", "uint64", "uintptr":
    61  		return Int64
    62  	case "bool":
    63  		return Bool
    64  	case "string", "error", "[]rune", "[]byte":
    65  		return String
    66  	case "float32":
    67  		return Float32
    68  	case "float64":
    69  		return Float64
    70  	case "multipart.FileHeader", "v3.FileModel":
    71  		return File
    72  	case "time.Time", "customtypes.Time":
    73  		return Time
    74  	case "decimal.Decimal": // simple treat decimal.Decimal as string
    75  		return Decimal
    76  	default:
    77  		return handleDefaultCase(ft)
    78  	}
    79  }
    80  
    81  func handleDefaultCase(ft string) *Schema {
    82  	if strings.HasPrefix(ft, "map[") {
    83  		elem := ft[strings.Index(ft, "]")+1:]
    84  		return &Schema{
    85  			Type: ObjectT,
    86  			AdditionalProperties: SchemaOf(astutils.FieldMeta{
    87  				Type: elem,
    88  			}),
    89  			XMapType: ft,
    90  		}
    91  	}
    92  	if strings.HasPrefix(ft, "[") {
    93  		elem := ft[strings.Index(ft, "]")+1:]
    94  		return &Schema{
    95  			Type: ArrayT,
    96  			Items: SchemaOf(astutils.FieldMeta{
    97  				Type: elem,
    98  			}),
    99  		}
   100  	}
   101  	re := regexp.MustCompile(`anonystruct«(.*)»`)
   102  	if re.MatchString(ft) {
   103  		result := re.FindStringSubmatch(ft)
   104  		var structmeta astutils.StructMeta
   105  		json.Unmarshal([]byte(result[1]), &structmeta)
   106  		schema := NewSchema(structmeta)
   107  		return &schema
   108  	}
   109  	var title string
   110  	if !strings.Contains(ft, ".") {
   111  		title = ft
   112  	}
   113  	if stringutils.IsEmpty(title) {
   114  		title = ft[strings.LastIndex(ft, ".")+1:]
   115  	}
   116  	if stringutils.IsNotEmpty(title) {
   117  		if unicode.IsUpper(rune(title[0])) {
   118  			if sliceutils.StringContains(SchemaNames, title) {
   119  				return &Schema{
   120  					Ref: "#/components/schemas/" + title,
   121  				}
   122  			}
   123  		}
   124  		if enumMeta, ok := Enums[title]; ok {
   125  			enumSchema := &Schema{
   126  				Type: StringT,
   127  				Enum: sliceutils.StringSlice2InterfaceSlice(enumMeta.Values),
   128  			}
   129  			if len(enumMeta.Values) > 0 {
   130  				enumSchema.Default = enumMeta.Values[0]
   131  			}
   132  			return enumSchema
   133  		}
   134  	}
   135  	return Any
   136  }
   137  
   138  var castFuncMap = map[string]string{
   139  	"bool":              "ToBool",
   140  	"float64":           "ToFloat64",
   141  	"float32":           "ToFloat32",
   142  	"int64":             "ToInt64",
   143  	"int32":             "ToInt32",
   144  	"int16":             "ToInt16",
   145  	"int8":              "ToInt8",
   146  	"int":               "ToInt",
   147  	"uint":              "ToUint",
   148  	"uint8":             "ToUint8",
   149  	"uint16":            "ToUint16",
   150  	"uint32":            "ToUint32",
   151  	"uint64":            "ToUint64",
   152  	"error":             "ToError",
   153  	"decimal.Decimal":   "ToDecimal",
   154  	"[]byte":            "ToByteSlice",
   155  	"[]rune":            "ToRuneSlice",
   156  	"[]interface{}":     "ToInterfaceSlice",
   157  	"[]bool":            "ToBoolSlice",
   158  	"[]int":             "ToIntSlice",
   159  	"[]float64":         "ToFloat64Slice",
   160  	"[]float32":         "ToFloat32Slice",
   161  	"[]int64":           "ToInt64Slice",
   162  	"[]int32":           "ToInt32Slice",
   163  	"[]int16":           "ToInt16Slice",
   164  	"[]int8":            "ToInt8Slice",
   165  	"[]uint":            "ToUintSlice",
   166  	"[]uint8":           "ToUint8Slice",
   167  	"[]uint16":          "ToUint16Slice",
   168  	"[]uint32":          "ToUint32Slice",
   169  	"[]uint64":          "ToUint64Slice",
   170  	"[]error":           "ToErrorSlice",
   171  	"[]decimal.Decimal": "ToDecimalSlice",
   172  	"[][]byte":          "ToByteSliceSlice",
   173  	"[][]rune":          "ToRuneSliceSlice",
   174  }
   175  
   176  func IsSupport(t string) bool {
   177  	if IsVarargs(t) {
   178  		t = ToSlice(t)
   179  	}
   180  	_, exists := castFuncMap[strings.TrimLeft(t, "*")]
   181  	return exists
   182  }
   183  
   184  func IsOptional(t string) bool {
   185  	return strings.HasPrefix(t, "*") || strings.HasPrefix(t, "...")
   186  }
   187  
   188  func IsSlice(t string) bool {
   189  	return strings.Contains(t, "[") || strings.HasPrefix(t, "...")
   190  }
   191  
   192  func IsVarargs(t string) bool {
   193  	return strings.HasPrefix(t, "...")
   194  }
   195  
   196  func ToSlice(t string) string {
   197  	return "[]" + strings.TrimPrefix(t, "...")
   198  }
   199  
   200  func CastFunc(t string) string {
   201  	if IsVarargs(t) {
   202  		t = ToSlice(t)
   203  	}
   204  	return castFuncMap[strings.TrimLeft(t, "*")]
   205  }
   206  
   207  // CopySchema as SchemaOf returns pointer, so deepcopy the schema the pointer points
   208  func CopySchema(field astutils.FieldMeta) Schema {
   209  	var schema Schema
   210  	err := copier.DeepCopy(SchemaOf(field), &schema)
   211  	if err != nil {
   212  		panic(err)
   213  	}
   214  	return schema
   215  }
   216  
   217  func RefAddDoc(schema *Schema, doc string) {
   218  	if stringutils.IsNotEmpty(schema.Ref) {
   219  		title := strings.TrimPrefix(schema.Ref, "#/components/schemas/")
   220  		temp := Schemas[title]
   221  		temp.Description = strings.Join([]string{doc, temp.Description}, "\n")
   222  		Schemas[title] = temp
   223  	} else {
   224  		schema.Description = doc
   225  	}
   226  }
   227  
   228  // NewSchema new schema from astutils.StructMeta
   229  func NewSchema(structmeta astutils.StructMeta) Schema {
   230  	properties := make(map[string]*Schema)
   231  	var required []string
   232  	for _, field := range structmeta.Fields {
   233  		fschema := CopySchema(field)
   234  		RefAddDoc(&fschema, strings.Join(field.Comments, "\n"))
   235  		properties[field.DocName] = &fschema
   236  		if !strings.HasPrefix(field.Type, "*") {
   237  			required = append(required, field.DocName)
   238  		}
   239  	}
   240  	return Schema{
   241  		Title:       structmeta.Name,
   242  		Type:        ObjectT,
   243  		Properties:  properties,
   244  		Description: strings.Join(structmeta.Comments, "\n"),
   245  		Required:    required,
   246  	}
   247  }
   248  
   249  // IsBuiltin check whether field is built-in type https://pkg.go.dev/builtin or not
   250  func IsBuiltin(field astutils.FieldMeta) bool {
   251  	simples := []interface{}{Int, Int64, Bool, String, Float32, Float64}
   252  	types := []interface{}{IntegerT, StringT, BooleanT, NumberT}
   253  	pschema := SchemaOf(field)
   254  	if sliceutils.Contains(simples, pschema) || (sliceutils.Contains(types, pschema.Type) && pschema.Format != BinaryF) {
   255  		return true
   256  	}
   257  	if pschema.Type == ArrayT && (sliceutils.Contains(simples, pschema.Items) || (sliceutils.Contains(types, pschema.Items.Type) && pschema.Items.Format != BinaryF)) {
   258  		return true
   259  	}
   260  	return false
   261  }
   262  
   263  // IsEnum check whether field is enum
   264  func IsEnum(field astutils.FieldMeta) bool {
   265  	pschema := SchemaOf(field)
   266  	return len(pschema.Enum) > 0 || (pschema.Type == ArrayT && len(pschema.Items.Enum) > 0)
   267  }
   268  
   269  // IsStruct check whether field is struct type
   270  func IsStruct(field astutils.FieldMeta) bool {
   271  	return stringutils.IsNotEmpty(SchemaOf(field).Ref)
   272  }
   273  
   274  // ElementType get element type string from slice
   275  func ElementType(t string) string {
   276  	if IsVarargs(t) {
   277  		return strings.TrimPrefix(t, "...")
   278  	}
   279  	return t[strings.Index(t, "]")+1:]
   280  }
   281  
   282  func LoadAPI(file string) API {
   283  	var (
   284  		docfile *os.File
   285  		err     error
   286  		docraw  []byte
   287  		api     API
   288  	)
   289  	if strings.HasPrefix(file, "http") {
   290  		link := file
   291  		client := resty.New()
   292  		client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
   293  		root, _ := os.Getwd()
   294  		client.SetOutputDirectory(root)
   295  		filename := ".openapi3"
   296  		_, err := client.R().
   297  			SetOutput(filename).
   298  			Get(link)
   299  		if err != nil {
   300  			panic(err)
   301  		}
   302  		file = filepath.Join(root, filename)
   303  		defer os.Remove(file)
   304  	}
   305  	if docfile, err = os.Open(file); err != nil {
   306  		panic(err)
   307  	}
   308  	defer docfile.Close()
   309  	if docraw, err = ioutil.ReadAll(docfile); err != nil {
   310  		panic(err)
   311  	}
   312  	if err = json.Unmarshal(docraw, &api); err != nil {
   313  		panic(err)
   314  	}
   315  	if stringutils.IsEmpty(api.Openapi) {
   316  		var doc openapi2.T
   317  		if err = json.Unmarshal(docraw, &doc); err != nil {
   318  			panic(err)
   319  		}
   320  		if stringutils.IsEmpty(doc.Swagger) {
   321  			panic("not support")
   322  		}
   323  		var doc1 *openapi3.T
   324  		doc1, err = openapi2conv.ToV3(&doc)
   325  		if err != nil {
   326  			panic(err)
   327  		}
   328  		doc1Json, _ := json.Marshal(doc1)
   329  		root, _ := os.Getwd()
   330  		spec := filepath.Join(root, "swaggerV2ToV3"+filepath.Ext(file))
   331  		os.WriteFile(spec, doc1Json, os.ModePerm)
   332  		copier.DeepCopy(doc1, &api)
   333  	}
   334  	return api
   335  }
   336  
   337  func ToOptional(t string) string {
   338  	if !strings.HasPrefix(t, "*") {
   339  		return "*" + t
   340  	}
   341  	return t
   342  }
   343  
   344  func IsEnumType(methods []astutils.MethodMeta) bool {
   345  	methodMap := make(map[string]struct{})
   346  	for _, item := range methods {
   347  		methodMap[item.String()] = struct{}{}
   348  	}
   349  	for _, item := range IEnumMethods {
   350  		if _, ok := methodMap[item]; !ok {
   351  			return false
   352  		}
   353  	}
   354  	return true
   355  }