github.com/alimy/mir/v4@v4.1.0/internal/reflex/field.go (about)

     1  // Copyright 2020 Michael Li <alimy@gility.net>. All rights reserved.
     2  // Use of this source code is governed by Apache License 2.0 that
     3  // can be found in the LICENSE file.
     4  
     5  package reflex
     6  
     7  import (
     8  	"errors"
     9  	"net/http"
    10  	"reflect"
    11  	"strings"
    12  
    13  	"github.com/alimy/mir/v4"
    14  	"github.com/alimy/mir/v4/assert"
    15  	"github.com/alimy/mir/v4/internal/utils"
    16  )
    17  
    18  var (
    19  	// error list
    20  	errNilType       tagError = "nil type is not valide"
    21  	errNotExist      tagError = "mir struct tag filed not exist"
    22  	errNoPathInfo    tagError = "mir struct tag not contains path info"
    23  	errNotValideType tagError = "not valide type, just struct and struct ptr is avalibale"
    24  	errMultGroupInfo tagError = "multiple group info in struct defined"
    25  	errMultChainInfo tagError = "multiple chain info in struct defined"
    26  )
    27  
    28  const (
    29  	mirPkgName = "github.com/alimy/mir/v4"
    30  )
    31  
    32  // tagError indicate error information
    33  type tagError string
    34  
    35  // Error error message string
    36  func (e tagError) Error() string {
    37  	return string(e)
    38  }
    39  
    40  // tagInfo indicate mir tag information in struct tag string
    41  type tagInfo struct {
    42  	isAnyMethod  bool                // isAnyMethod indicate whether method is Any
    43  	isFieldChain bool                // isFieldChain indicate whether method is need field chain
    44  	isUseContext bool                // isUseContext indicate whether method is just use Context
    45  	methods      utils.HttpMethodSet // method indicate methods information in struct tag string
    46  	host         string              // host indicate host information in struct tag string
    47  	path         string              // path indicate path information in struct tag string
    48  	queries      []string            // queries indicate path information in struct tag string
    49  	isGroup      bool                // indicate whether a group field
    50  	isChain      bool                // indicate whether a chain field
    51  	group        string              // indicate group information in struct tag string
    52  	chainFunc    string              // indicate chain function information in struct tag string
    53  	handler      string              // indicate handler information in struct tag string
    54  	fieldName    string              // indicate field name
    55  	isBindIn     bool
    56  	isRenderOut  bool
    57  	in           reflect.Type
    58  	out          reflect.Type
    59  	inOuts       []reflect.Type
    60  	comment      string // indicate comment info (not support now)
    61  }
    62  
    63  // tagInfoFrom build tagInfo from field
    64  func (r *reflex) tagInfoFrom(field reflect.StructField, pkgPath string) (*tagInfo, error) {
    65  	info := &tagInfo{
    66  		methods: make(utils.HttpMethodSet, 1),
    67  	}
    68  
    69  	// lookup mir tag info from struct field
    70  	tag, exist := field.Tag.Lookup(r.tagName)
    71  	if !exist {
    72  		return nil, errNotExist
    73  	}
    74  
    75  	// Skip leading space.
    76  	i := 0
    77  	for i < len(tag) && tag[i] == ' ' {
    78  		i++
    79  	}
    80  	tag = tag[i:]
    81  
    82  	// group info or method info or chain info
    83  	info.fieldName = field.Name
    84  	switch field.Type.Kind() {
    85  	case reflect.Interface:
    86  		if field.Type.PkgPath() != mirPkgName {
    87  			return nil, errors.New("not supported filed type")
    88  		}
    89  		switch field.Type.Name() {
    90  		case "Chain":
    91  			info.isChain = true
    92  			return info, nil
    93  		case "Group":
    94  			info.isGroup = true
    95  			info.group = tag
    96  			return info, nil
    97  		default:
    98  			return nil, errors.New("not supported filed type")
    99  		}
   100  	case reflect.Func:
   101  		ft := field.Type
   102  		numIn := ft.NumIn()
   103  		numOut := ft.NumOut()
   104  		if numOut > 1 {
   105  			return nil, errors.New("func field just need one most return value")
   106  		}
   107  		if numIn > 0 {
   108  			// request type in latest in argument if declared
   109  			it := ft.In(numIn - 1)
   110  			if it.Kind() == reflect.Struct {
   111  				cts, err := CheckStruct(it, pkgPath)
   112  				if err != nil {
   113  					return nil, err
   114  				}
   115  				info.in = it
   116  				if it.PkgPath() != pkgPath {
   117  					info.isBindIn = assert.AssertBinding(reflect.New(it).Interface())
   118  				}
   119  				info.inOuts = append(info.inOuts, cts...)
   120  
   121  				// minus numIn to ignore latest in argument that had processed
   122  				numIn--
   123  			}
   124  
   125  			// process other in argument
   126  			for i := numIn - 1; i >= 0; i-- {
   127  				it = ft.In(i)
   128  				if it.PkgPath() != mirPkgName {
   129  					continue
   130  				}
   131  				switch it.Name() {
   132  				case "Get":
   133  					info.methods.Add(mir.MethodGet)
   134  				case "Put":
   135  					info.methods.Add(mir.MethodPut)
   136  				case "Post":
   137  					info.methods.Add(mir.MethodPost)
   138  				case "Delete":
   139  					info.methods.Add(mir.MethodDelete)
   140  				case "Head":
   141  					info.methods.Add(mir.MethodHead)
   142  				case "Options":
   143  					info.methods.Add(mir.MethodOptions)
   144  				case "Patch":
   145  					info.methods.Add(mir.MethodPatch)
   146  				case "Trace":
   147  					info.methods.Add(mir.MethodTrace)
   148  				case "Connect":
   149  					info.methods.Add(mir.MethodConnect)
   150  				case "Any":
   151  					info.isAnyMethod = true
   152  					info.methods.Add(mir.HttpMethods...)
   153  				case "Chain":
   154  					info.isFieldChain = true
   155  				case "Context":
   156  					info.isUseContext = true
   157  				}
   158  			}
   159  		}
   160  		// process special case for not set methods
   161  		if len(info.methods) == 0 {
   162  			info.isAnyMethod = true
   163  			info.methods.Add(mir.HttpMethods...)
   164  		}
   165  		if numOut == 1 {
   166  			ot := ft.Out(i)
   167  			if ot.Kind() != reflect.Struct {
   168  				return nil, errors.New("func field must return value is need struct type")
   169  			}
   170  			cts, err := CheckStruct(ot, pkgPath)
   171  			if err != nil {
   172  				return nil, err
   173  			}
   174  			info.out = ot
   175  			if ot.PkgPath() != pkgPath {
   176  				info.isRenderOut = assert.AssertRender(reflect.New(ot).Interface())
   177  			}
   178  			info.inOuts = append(info.inOuts, cts...)
   179  		}
   180  	default:
   181  		return nil, errors.New("not supported filed type")
   182  	}
   183  
   184  	// host info
   185  	if len(tag) > 2 && tag[0] == '/' && tag[1] == '/' {
   186  		i := 2
   187  		for i < len(tag) && tag[i] != '/' {
   188  			i++
   189  		}
   190  		info.host = tag[2:i]
   191  		tag = tag[i:]
   192  	}
   193  
   194  	// path info. must have path info if not a group field
   195  	if len(tag) == 0 && !info.isGroup {
   196  		return nil, errNoPathInfo
   197  	}
   198  	for i = 0; i < len(tag) && tag[i] != '#'; i++ {
   199  		if !r.noneQuery && tag[i] == '?' {
   200  			break
   201  		}
   202  	}
   203  	info.path = tag[0:i]
   204  	tag = tag[i:]
   205  
   206  	// queries and handler info
   207  	for len(tag) != 0 {
   208  		switch tag[0] {
   209  		case '#':
   210  			i := 1
   211  			for i < len(tag) && tag[i] != '?' {
   212  				i++
   213  			}
   214  			handlerStr := tag[1:i]
   215  			tag = tag[i:]
   216  			if handlerStr != "" {
   217  				if handlerStr[0] == '-' { // just contain chain func info
   218  					info.chainFunc = handlerStr[1:]
   219  				} else { // contain handler and inline chain info like #Handler&ChainFunc
   220  					handlerChains := strings.Split(handlerStr, "&")
   221  					info.handler = handlerChains[0]
   222  					if len(handlerChains) > 1 { // extract chain func
   223  						info.chainFunc = handlerChains[1]
   224  					}
   225  				}
   226  			}
   227  		case '?':
   228  			i := 1
   229  			for i < len(tag) && tag[i] != '#' {
   230  				i++
   231  			}
   232  			queryStr := tag[1:i]
   233  			if queryStr != "" {
   234  				info.queries = r.inflateQuery(queryStr)
   235  			}
   236  			tag = tag[i:]
   237  		}
   238  	}
   239  
   240  	// check handler if not group field
   241  	if info.handler == "" {
   242  		// assign handler name
   243  		info.handler = utils.UpperFirst(field.Name)
   244  	}
   245  
   246  	return info, nil
   247  }
   248  
   249  func (r *reflex) anyMethodsFromTag(value string) ([]string, bool) {
   250  	anyMethod := strings.TrimSpace(value)
   251  	methods := strings.Split(anyMethod, ",")
   252  	res := make([]string, 0, len(methods))
   253  	for _, method := range methods {
   254  		method = strings.ToUpper(strings.TrimSpace(method))
   255  		switch method {
   256  		case http.MethodGet,
   257  			http.MethodHead,
   258  			http.MethodPost,
   259  			http.MethodPut,
   260  			http.MethodPatch,
   261  			http.MethodDelete,
   262  			http.MethodConnect,
   263  			http.MethodOptions,
   264  			http.MethodTrace:
   265  			res = append(res, method)
   266  		}
   267  	}
   268  	if len(res) > 0 {
   269  		return res, true
   270  	}
   271  	return nil, false
   272  }
   273  
   274  func (r *reflex) inflateQuery(qs string) []string {
   275  	items := strings.Split(qs, "&")
   276  	res := make([]string, 0, len(items)*2)
   277  	for _, q := range items {
   278  		kv := strings.Split(q, "=")
   279  		if len(kv) == 2 {
   280  			res = append(res, kv...)
   281  		}
   282  	}
   283  	return res
   284  }
   285  
   286  // valueByName return field value by field name
   287  func (r *reflex) valueByName(value reflect.Value, name string) any {
   288  	if fieldValue := value.FieldByName(name); !fieldValue.IsNil() {
   289  		return fieldValue.Elem().Interface()
   290  	}
   291  	return nil
   292  }