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 }