alpha: v0.0.1
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | .idea | ||||||
|  | .vscode | ||||||
|  | .DS_Store | ||||||
							
								
								
									
										44
									
								
								app.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type App struct { | ||||||
|  | 	*RouterGroup | ||||||
|  | 	config *Config | ||||||
|  | 	router *router | ||||||
|  | 	groups []*RouterGroup | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) { | ||||||
|  | 	c := newContext(a, writer, request) | ||||||
|  |  | ||||||
|  | 	for _, group := range a.groups { | ||||||
|  | 		if strings.HasPrefix(request.URL.Path, group.prefix) { | ||||||
|  | 			c.handlers = append(c.handlers, group.middlewares...) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := a.router.handle(c); err != nil { | ||||||
|  | 		var ne = &Err{} | ||||||
|  |  | ||||||
|  | 		if errors.As(err, ne) { | ||||||
|  | 			writer.WriteHeader(ne.Status) | ||||||
|  | 		} else { | ||||||
|  | 			writer.WriteHeader(500) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		_, _ = writer.Write([]byte(err.Error())) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *App) Run(address string) error { | ||||||
|  | 	if !a.config.DisableBanner { | ||||||
|  | 		fmt.Println(banner + "nf serve at: " + address + "\n") | ||||||
|  | 	} | ||||||
|  | 	return http.ListenAndServe(address, a) | ||||||
|  | } | ||||||
							
								
								
									
										156
									
								
								ctx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								ctx.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Ctx struct { | ||||||
|  | 	// origin objects | ||||||
|  | 	Writer  http.ResponseWriter | ||||||
|  | 	Request *http.Request | ||||||
|  | 	// request info | ||||||
|  | 	path   string | ||||||
|  | 	Method string | ||||||
|  | 	// response info | ||||||
|  | 	StatusCode int | ||||||
|  |  | ||||||
|  | 	app      *App | ||||||
|  | 	params   map[string]string | ||||||
|  | 	index    int | ||||||
|  | 	handlers []HandlerFunc | ||||||
|  | 	locals   map[string]any | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx { | ||||||
|  | 	return &Ctx{ | ||||||
|  | 		Writer:  writer, | ||||||
|  | 		Request: request, | ||||||
|  | 		path:    request.URL.Path, | ||||||
|  | 		Method:  request.Method, | ||||||
|  |  | ||||||
|  | 		app:      app, | ||||||
|  | 		index:    -1, | ||||||
|  | 		locals:   map[string]any{}, | ||||||
|  | 		handlers: make([]HandlerFunc, 0), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Locals(key string, value ...any) any { | ||||||
|  | 	data := c.locals[key] | ||||||
|  | 	if len(value) > 0 { | ||||||
|  | 		c.locals[key] = value[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return data | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Path(overWrite ...string) string { | ||||||
|  | 	path := c.Request.URL.Path | ||||||
|  | 	if len(overWrite) > 0 && overWrite[0] != "" { | ||||||
|  | 		c.Request.URL.Path = overWrite[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return path | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Next() error { | ||||||
|  | 	c.index++ | ||||||
|  | 	s := len(c.handlers) | ||||||
|  | 	for ; c.index < s; c.index++ { | ||||||
|  | 		if err := c.handlers[c.index](c); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* =============================================================== | ||||||
|  | || Handle Ctx Request Part | ||||||
|  | =============================================================== */ | ||||||
|  |  | ||||||
|  | func (c *Ctx) verify() error { | ||||||
|  | 	// 验证 body size | ||||||
|  | 	if c.app.config.BodyLimit != -1 && c.Request.ContentLength > c.app.config.BodyLimit { | ||||||
|  | 		return NewNFError(413, "Content Too Large") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Param(key string) string { | ||||||
|  | 	return c.params[key] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Form(key string) string { | ||||||
|  | 	return c.Request.FormValue(key) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Query(key string) string { | ||||||
|  | 	return c.Request.URL.Query().Get(key) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Get(key string, defaultValue ...string) string { | ||||||
|  | 	value := c.Request.Header.Get(key) | ||||||
|  | 	if value == "" && len(defaultValue) > 0 { | ||||||
|  | 		return defaultValue[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return value | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) BodyParser(out interface{}) error { | ||||||
|  | 	var ( | ||||||
|  | 		err   error | ||||||
|  | 		ctype = strings.ToLower(c.Request.Header.Get("Content-Type")) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	log.Printf("BodyParser: Content-Type=%s", ctype) | ||||||
|  |  | ||||||
|  | 	ctype = parseVendorSpecificContentType(ctype) | ||||||
|  |  | ||||||
|  | 	ctypeEnd := strings.IndexByte(ctype, ';') | ||||||
|  | 	if ctypeEnd != -1 { | ||||||
|  | 		ctype = ctype[:ctypeEnd] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasSuffix(ctype, "json") { | ||||||
|  | 		bs, err := io.ReadAll(c.Request.Body) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Printf("BodyParser: read all err=%v", err) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		c.Request.Body = io.NopCloser(bytes.NewReader(bs)) | ||||||
|  |  | ||||||
|  | 		return json.Unmarshal(bs, out) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(ctype, MIMEApplicationForm) { | ||||||
|  |  | ||||||
|  | 		if err = c.Request.ParseForm(); err != nil { | ||||||
|  | 			return NewNFError(400, err.Error()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return parseToStruct("form", out, c.Request.Form) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if strings.HasPrefix(ctype, MIMEMultipartForm) { | ||||||
|  | 		if err = c.Request.ParseMultipartForm(c.app.config.BodyLimit); err != nil { | ||||||
|  | 			return NewNFError(400, err.Error()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return parseToStruct("form", out, c.Request.PostForm) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return NewNFError(422, "Unprocessable Content") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) QueryParser(out interface{}) error { | ||||||
|  | 	return parseToStruct("query", out, c.Request.URL.Query()) | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								error.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								error.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import "strconv" | ||||||
|  |  | ||||||
|  | type Err struct { | ||||||
|  | 	Status int | ||||||
|  | 	Msg    string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n Err) Error() string { | ||||||
|  | 	return strconv.Itoa(n.Status) + " " + n.Msg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewNFError(status int, msg string) Err { | ||||||
|  | 	return Err{Status: status, Msg: msg} | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								group.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								group.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type RouterGroup struct { | ||||||
|  | 	prefix      string | ||||||
|  | 	middlewares []HandlerFunc // support middleware | ||||||
|  | 	parent      *RouterGroup  // support nesting | ||||||
|  | 	app         *App          // all groups share a Engine instance | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Group is defined to create a new RouterGroup | ||||||
|  | // remember all groups share the same Engine instance | ||||||
|  | func (group *RouterGroup) Group(prefix string) *RouterGroup { | ||||||
|  | 	app := group.app | ||||||
|  | 	newGroup := &RouterGroup{ | ||||||
|  | 		prefix: group.prefix + prefix, | ||||||
|  | 		parent: group, | ||||||
|  | 		app:    app, | ||||||
|  | 	} | ||||||
|  | 	app.groups = append(app.groups, newGroup) | ||||||
|  | 	return newGroup | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) addRoute(method string, comp string, handlers ...HandlerFunc) { | ||||||
|  | 	verifyHandlers(comp, handlers...) | ||||||
|  | 	pattern := group.prefix + comp | ||||||
|  | 	log.Printf("Add Route %4s - %s", method, pattern) | ||||||
|  | 	group.app.router.addRoute(method, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Get(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodGet, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Post(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodPost, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Put(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodPut, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Delete(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodDelete, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Patch(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodPatch, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Head(pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	group.addRoute(http.MethodHead, pattern, handlers...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (group *RouterGroup) Use(middlewares ...HandlerFunc) { | ||||||
|  | 	group.middlewares = append(group.middlewares, middlewares...) | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | type HandlerFunc func(*Ctx) error | ||||||
							
								
								
									
										27
									
								
								internal/schema/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								internal/schema/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. | ||||||
|  |  | ||||||
|  | Redistribution and use in source and binary forms, with or without | ||||||
|  | modification, are permitted provided that the following conditions are | ||||||
|  | met: | ||||||
|  |  | ||||||
|  | 	 * Redistributions of source code must retain the above copyright | ||||||
|  | notice, this list of conditions and the following disclaimer. | ||||||
|  | 	 * Redistributions in binary form must reproduce the above | ||||||
|  | copyright notice, this list of conditions and the following disclaimer | ||||||
|  | in the documentation and/or other materials provided with the | ||||||
|  | distribution. | ||||||
|  | 	 * Neither the name of Google Inc. nor the names of its | ||||||
|  | contributors may be used to endorse or promote products derived from | ||||||
|  | this software without specific prior written permission. | ||||||
|  |  | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||||
|  | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||||
|  | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||||
|  | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||||
|  | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||||
|  | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||||
|  | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||||
|  | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||||
|  | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||||
|  | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
							
								
								
									
										305
									
								
								internal/schema/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										305
									
								
								internal/schema/cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,305 @@ | |||||||
|  | // Copyright 2012 The Gorilla Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package schema | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var errInvalidPath = errors.New("schema: invalid path") | ||||||
|  |  | ||||||
|  | // newCache returns a new cache. | ||||||
|  | func newCache() *cache { | ||||||
|  | 	c := cache{ | ||||||
|  | 		m:       make(map[reflect.Type]*structInfo), | ||||||
|  | 		regconv: make(map[reflect.Type]Converter), | ||||||
|  | 		tag:     "schema", | ||||||
|  | 	} | ||||||
|  | 	return &c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // cache caches meta-data about a struct. | ||||||
|  | type cache struct { | ||||||
|  | 	l       sync.RWMutex | ||||||
|  | 	m       map[reflect.Type]*structInfo | ||||||
|  | 	regconv map[reflect.Type]Converter | ||||||
|  | 	tag     string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // registerConverter registers a converter function for a custom type. | ||||||
|  | func (c *cache) registerConverter(value interface{}, converterFunc Converter) { | ||||||
|  | 	c.regconv[reflect.TypeOf(value)] = converterFunc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // parsePath parses a path in dotted notation verifying that it is a valid | ||||||
|  | // path to a struct field. | ||||||
|  | // | ||||||
|  | // It returns "path parts" which contain indices to fields to be used by | ||||||
|  | // reflect.Value.FieldByString(). Multiple parts are required for slices of | ||||||
|  | // structs. | ||||||
|  | func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { | ||||||
|  | 	var struc *structInfo | ||||||
|  | 	var field *fieldInfo | ||||||
|  | 	var index64 int64 | ||||||
|  | 	var err error | ||||||
|  | 	parts := make([]pathPart, 0) | ||||||
|  | 	path := make([]string, 0) | ||||||
|  | 	keys := strings.Split(p, ".") | ||||||
|  | 	for i := 0; i < len(keys); i++ { | ||||||
|  | 		if t.Kind() != reflect.Struct { | ||||||
|  | 			return nil, errInvalidPath | ||||||
|  | 		} | ||||||
|  | 		if struc = c.get(t); struc == nil { | ||||||
|  | 			return nil, errInvalidPath | ||||||
|  | 		} | ||||||
|  | 		if field = struc.get(keys[i]); field == nil { | ||||||
|  | 			return nil, errInvalidPath | ||||||
|  | 		} | ||||||
|  | 		// Valid field. Append index. | ||||||
|  | 		path = append(path, field.name) | ||||||
|  | 		if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { | ||||||
|  | 			// Parse a special case: slices of structs. | ||||||
|  | 			// i+1 must be the slice index. | ||||||
|  | 			// | ||||||
|  | 			// Now that struct can implements TextUnmarshaler interface, | ||||||
|  | 			// we don't need to force the struct's fields to appear in the path. | ||||||
|  | 			// So checking i+2 is not necessary anymore. | ||||||
|  | 			i++ | ||||||
|  | 			if i+1 > len(keys) { | ||||||
|  | 				return nil, errInvalidPath | ||||||
|  | 			} | ||||||
|  | 			if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { | ||||||
|  | 				return nil, errInvalidPath | ||||||
|  | 			} | ||||||
|  | 			parts = append(parts, pathPart{ | ||||||
|  | 				path:  path, | ||||||
|  | 				field: field, | ||||||
|  | 				index: int(index64), | ||||||
|  | 			}) | ||||||
|  | 			path = make([]string, 0) | ||||||
|  |  | ||||||
|  | 			// Get the next struct type, dropping ptrs. | ||||||
|  | 			if field.typ.Kind() == reflect.Ptr { | ||||||
|  | 				t = field.typ.Elem() | ||||||
|  | 			} else { | ||||||
|  | 				t = field.typ | ||||||
|  | 			} | ||||||
|  | 			if t.Kind() == reflect.Slice { | ||||||
|  | 				t = t.Elem() | ||||||
|  | 				if t.Kind() == reflect.Ptr { | ||||||
|  | 					t = t.Elem() | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else if field.typ.Kind() == reflect.Ptr { | ||||||
|  | 			t = field.typ.Elem() | ||||||
|  | 		} else { | ||||||
|  | 			t = field.typ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// Add the remaining. | ||||||
|  | 	parts = append(parts, pathPart{ | ||||||
|  | 		path:  path, | ||||||
|  | 		field: field, | ||||||
|  | 		index: -1, | ||||||
|  | 	}) | ||||||
|  | 	return parts, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // get returns a cached structInfo, creating it if necessary. | ||||||
|  | func (c *cache) get(t reflect.Type) *structInfo { | ||||||
|  | 	c.l.RLock() | ||||||
|  | 	info := c.m[t] | ||||||
|  | 	c.l.RUnlock() | ||||||
|  | 	if info == nil { | ||||||
|  | 		info = c.create(t, "") | ||||||
|  | 		c.l.Lock() | ||||||
|  | 		c.m[t] = info | ||||||
|  | 		c.l.Unlock() | ||||||
|  | 	} | ||||||
|  | 	return info | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // create creates a structInfo with meta-data about a struct. | ||||||
|  | func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { | ||||||
|  | 	info := &structInfo{} | ||||||
|  | 	var anonymousInfos []*structInfo | ||||||
|  | 	for i := 0; i < t.NumField(); i++ { | ||||||
|  | 		if f := c.createField(t.Field(i), parentAlias); f != nil { | ||||||
|  | 			info.fields = append(info.fields, f) | ||||||
|  | 			if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { | ||||||
|  | 				anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for i, a := range anonymousInfos { | ||||||
|  | 		others := []*structInfo{info} | ||||||
|  | 		others = append(others, anonymousInfos[:i]...) | ||||||
|  | 		others = append(others, anonymousInfos[i+1:]...) | ||||||
|  | 		for _, f := range a.fields { | ||||||
|  | 			if !containsAlias(others, f.alias) { | ||||||
|  | 				info.fields = append(info.fields, f) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return info | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createField creates a fieldInfo for the given field. | ||||||
|  | func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { | ||||||
|  | 	alias, options := fieldAlias(field, c.tag) | ||||||
|  | 	if alias == "-" { | ||||||
|  | 		// Ignore this field. | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	canonicalAlias := alias | ||||||
|  | 	if parentAlias != "" { | ||||||
|  | 		canonicalAlias = parentAlias + "." + alias | ||||||
|  | 	} | ||||||
|  | 	// Check if the type is supported and don't cache it if not. | ||||||
|  | 	// First let's get the basic type. | ||||||
|  | 	isSlice, isStruct := false, false | ||||||
|  | 	ft := field.Type | ||||||
|  | 	m := isTextUnmarshaler(reflect.Zero(ft)) | ||||||
|  | 	if ft.Kind() == reflect.Ptr { | ||||||
|  | 		ft = ft.Elem() | ||||||
|  | 	} | ||||||
|  | 	if isSlice = ft.Kind() == reflect.Slice; isSlice { | ||||||
|  | 		ft = ft.Elem() | ||||||
|  | 		if ft.Kind() == reflect.Ptr { | ||||||
|  | 			ft = ft.Elem() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if ft.Kind() == reflect.Array { | ||||||
|  | 		ft = ft.Elem() | ||||||
|  | 		if ft.Kind() == reflect.Ptr { | ||||||
|  | 			ft = ft.Elem() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if isStruct = ft.Kind() == reflect.Struct; !isStruct { | ||||||
|  | 		if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { | ||||||
|  | 			// Type is not supported. | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &fieldInfo{ | ||||||
|  | 		typ:              field.Type, | ||||||
|  | 		name:             field.Name, | ||||||
|  | 		alias:            alias, | ||||||
|  | 		canonicalAlias:   canonicalAlias, | ||||||
|  | 		unmarshalerInfo:  m, | ||||||
|  | 		isSliceOfStructs: isSlice && isStruct, | ||||||
|  | 		isAnonymous:      field.Anonymous, | ||||||
|  | 		isRequired:       options.Contains("required"), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // converter returns the converter for a type. | ||||||
|  | func (c *cache) converter(t reflect.Type) Converter { | ||||||
|  | 	return c.regconv[t] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ---------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | type structInfo struct { | ||||||
|  | 	fields []*fieldInfo | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (i *structInfo) get(alias string) *fieldInfo { | ||||||
|  | 	for _, field := range i.fields { | ||||||
|  | 		if strings.EqualFold(field.alias, alias) { | ||||||
|  | 			return field | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func containsAlias(infos []*structInfo, alias string) bool { | ||||||
|  | 	for _, info := range infos { | ||||||
|  | 		if info.get(alias) != nil { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type fieldInfo struct { | ||||||
|  | 	typ reflect.Type | ||||||
|  | 	// name is the field name in the struct. | ||||||
|  | 	name  string | ||||||
|  | 	alias string | ||||||
|  | 	// canonicalAlias is almost the same as the alias, but is prefixed with | ||||||
|  | 	// an embedded struct field alias in dotted notation if this field is | ||||||
|  | 	// promoted from the struct. | ||||||
|  | 	// For instance, if the alias is "N" and this field is an embedded field | ||||||
|  | 	// in a struct "X", canonicalAlias will be "X.N". | ||||||
|  | 	canonicalAlias string | ||||||
|  | 	// unmarshalerInfo contains information regarding the | ||||||
|  | 	// encoding.TextUnmarshaler implementation of the field type. | ||||||
|  | 	unmarshalerInfo unmarshaler | ||||||
|  | 	// isSliceOfStructs indicates if the field type is a slice of structs. | ||||||
|  | 	isSliceOfStructs bool | ||||||
|  | 	// isAnonymous indicates whether the field is embedded in the struct. | ||||||
|  | 	isAnonymous bool | ||||||
|  | 	isRequired  bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (f *fieldInfo) paths(prefix string) []string { | ||||||
|  | 	if f.alias == f.canonicalAlias { | ||||||
|  | 		return []string{prefix + f.alias} | ||||||
|  | 	} | ||||||
|  | 	return []string{prefix + f.alias, prefix + f.canonicalAlias} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type pathPart struct { | ||||||
|  | 	field *fieldInfo | ||||||
|  | 	path  []string // path to the field: walks structs using field names. | ||||||
|  | 	index int      // struct index in slices of structs. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ---------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | func indirectType(typ reflect.Type) reflect.Type { | ||||||
|  | 	if typ.Kind() == reflect.Ptr { | ||||||
|  | 		return typ.Elem() | ||||||
|  | 	} | ||||||
|  | 	return typ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // fieldAlias parses a field tag to get a field alias. | ||||||
|  | func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { | ||||||
|  | 	if tag := field.Tag.Get(tagName); tag != "" { | ||||||
|  | 		alias, options = parseTag(tag) | ||||||
|  | 	} | ||||||
|  | 	if alias == "" { | ||||||
|  | 		alias = field.Name | ||||||
|  | 	} | ||||||
|  | 	return alias, options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // tagOptions is the string following a comma in a struct field's tag, or | ||||||
|  | // the empty string. It does not include the leading comma. | ||||||
|  | type tagOptions []string | ||||||
|  |  | ||||||
|  | // parseTag splits a struct field's url tag into its name and comma-separated | ||||||
|  | // options. | ||||||
|  | func parseTag(tag string) (string, tagOptions) { | ||||||
|  | 	s := strings.Split(tag, ",") | ||||||
|  | 	return s[0], s[1:] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Contains checks whether the tagOptions contains the specified option. | ||||||
|  | func (o tagOptions) Contains(option string) bool { | ||||||
|  | 	for _, s := range o { | ||||||
|  | 		if s == option { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
							
								
								
									
										145
									
								
								internal/schema/converter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								internal/schema/converter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | |||||||
|  | // Copyright 2012 The Gorilla Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package schema | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"strconv" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Converter func(string) reflect.Value | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	invalidValue = reflect.Value{} | ||||||
|  | 	boolType     = reflect.Bool | ||||||
|  | 	float32Type  = reflect.Float32 | ||||||
|  | 	float64Type  = reflect.Float64 | ||||||
|  | 	intType      = reflect.Int | ||||||
|  | 	int8Type     = reflect.Int8 | ||||||
|  | 	int16Type    = reflect.Int16 | ||||||
|  | 	int32Type    = reflect.Int32 | ||||||
|  | 	int64Type    = reflect.Int64 | ||||||
|  | 	stringType   = reflect.String | ||||||
|  | 	uintType     = reflect.Uint | ||||||
|  | 	uint8Type    = reflect.Uint8 | ||||||
|  | 	uint16Type   = reflect.Uint16 | ||||||
|  | 	uint32Type   = reflect.Uint32 | ||||||
|  | 	uint64Type   = reflect.Uint64 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Default converters for basic types. | ||||||
|  | var builtinConverters = map[reflect.Kind]Converter{ | ||||||
|  | 	boolType:    convertBool, | ||||||
|  | 	float32Type: convertFloat32, | ||||||
|  | 	float64Type: convertFloat64, | ||||||
|  | 	intType:     convertInt, | ||||||
|  | 	int8Type:    convertInt8, | ||||||
|  | 	int16Type:   convertInt16, | ||||||
|  | 	int32Type:   convertInt32, | ||||||
|  | 	int64Type:   convertInt64, | ||||||
|  | 	stringType:  convertString, | ||||||
|  | 	uintType:    convertUint, | ||||||
|  | 	uint8Type:   convertUint8, | ||||||
|  | 	uint16Type:  convertUint16, | ||||||
|  | 	uint32Type:  convertUint32, | ||||||
|  | 	uint64Type:  convertUint64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertBool(value string) reflect.Value { | ||||||
|  | 	if value == "on" { | ||||||
|  | 		return reflect.ValueOf(true) | ||||||
|  | 	} else if v, err := strconv.ParseBool(value); err == nil { | ||||||
|  | 		return reflect.ValueOf(v) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertFloat32(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseFloat(value, 32); err == nil { | ||||||
|  | 		return reflect.ValueOf(float32(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertFloat64(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseFloat(value, 64); err == nil { | ||||||
|  | 		return reflect.ValueOf(v) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertInt(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseInt(value, 10, 0); err == nil { | ||||||
|  | 		return reflect.ValueOf(int(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertInt8(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseInt(value, 10, 8); err == nil { | ||||||
|  | 		return reflect.ValueOf(int8(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertInt16(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseInt(value, 10, 16); err == nil { | ||||||
|  | 		return reflect.ValueOf(int16(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertInt32(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseInt(value, 10, 32); err == nil { | ||||||
|  | 		return reflect.ValueOf(int32(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertInt64(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseInt(value, 10, 64); err == nil { | ||||||
|  | 		return reflect.ValueOf(v) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertString(value string) reflect.Value { | ||||||
|  | 	return reflect.ValueOf(value) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertUint(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseUint(value, 10, 0); err == nil { | ||||||
|  | 		return reflect.ValueOf(uint(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertUint8(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseUint(value, 10, 8); err == nil { | ||||||
|  | 		return reflect.ValueOf(uint8(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertUint16(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseUint(value, 10, 16); err == nil { | ||||||
|  | 		return reflect.ValueOf(uint16(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertUint32(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseUint(value, 10, 32); err == nil { | ||||||
|  | 		return reflect.ValueOf(uint32(v)) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertUint64(value string) reflect.Value { | ||||||
|  | 	if v, err := strconv.ParseUint(value, 10, 64); err == nil { | ||||||
|  | 		return reflect.ValueOf(v) | ||||||
|  | 	} | ||||||
|  | 	return invalidValue | ||||||
|  | } | ||||||
							
								
								
									
										534
									
								
								internal/schema/decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								internal/schema/decoder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,534 @@ | |||||||
|  | // Copyright 2012 The Gorilla Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package schema | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewDecoder returns a new Decoder. | ||||||
|  | func NewDecoder() *Decoder { | ||||||
|  | 	return &Decoder{cache: newCache()} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Decoder decodes values from a map[string][]string to a struct. | ||||||
|  | type Decoder struct { | ||||||
|  | 	cache             *cache | ||||||
|  | 	zeroEmpty         bool | ||||||
|  | 	ignoreUnknownKeys bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetAliasTag changes the tag used to locate custom field aliases. | ||||||
|  | // The default tag is "schema". | ||||||
|  | func (d *Decoder) SetAliasTag(tag string) { | ||||||
|  | 	d.cache.tag = tag | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ZeroEmpty controls the behaviour when the decoder encounters empty values | ||||||
|  | // in a map. | ||||||
|  | // If z is true and a key in the map has the empty string as a value | ||||||
|  | // then the corresponding struct field is set to the zero value. | ||||||
|  | // If z is false then empty strings are ignored. | ||||||
|  | // | ||||||
|  | // The default value is false, that is empty values do not change | ||||||
|  | // the value of the struct field. | ||||||
|  | func (d *Decoder) ZeroEmpty(z bool) { | ||||||
|  | 	d.zeroEmpty = z | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown | ||||||
|  | // keys in the map. | ||||||
|  | // If i is true and an unknown field is encountered, it is ignored. This is | ||||||
|  | // similar to how unknown keys are handled by encoding/json. | ||||||
|  | // If i is false then Decode will return an error. Note that any valid keys | ||||||
|  | // will still be decoded in to the target struct. | ||||||
|  | // | ||||||
|  | // To preserve backwards compatibility, the default value is false. | ||||||
|  | func (d *Decoder) IgnoreUnknownKeys(i bool) { | ||||||
|  | 	d.ignoreUnknownKeys = i | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RegisterConverter registers a converter function for a custom type. | ||||||
|  | func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { | ||||||
|  | 	d.cache.registerConverter(value, converterFunc) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Decode decodes a map[string][]string to a struct. | ||||||
|  | // | ||||||
|  | // The first parameter must be a pointer to a struct. | ||||||
|  | // | ||||||
|  | // The second parameter is a map, typically url.Values from an HTTP request. | ||||||
|  | // Keys are "paths" in dotted notation to the struct fields and nested structs. | ||||||
|  | // | ||||||
|  | // See the package documentation for a full explanation of the mechanics. | ||||||
|  | func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { | ||||||
|  | 	v := reflect.ValueOf(dst) | ||||||
|  | 	if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { | ||||||
|  | 		return errors.New("schema: interface must be a pointer to struct") | ||||||
|  | 	} | ||||||
|  | 	v = v.Elem() | ||||||
|  | 	t := v.Type() | ||||||
|  | 	multiError := MultiError{} | ||||||
|  | 	for path, values := range src { | ||||||
|  | 		if parts, err := d.cache.parsePath(path, t); err == nil { | ||||||
|  | 			if err = d.decode(v, path, parts, values); err != nil { | ||||||
|  | 				multiError[path] = err | ||||||
|  | 			} | ||||||
|  | 		} else if !d.ignoreUnknownKeys { | ||||||
|  | 			multiError[path] = UnknownKeyError{Key: path} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	multiError.merge(d.checkRequired(t, src)) | ||||||
|  | 	if len(multiError) > 0 { | ||||||
|  | 		return multiError | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // checkRequired checks whether required fields are empty | ||||||
|  | // | ||||||
|  | // check type t recursively if t has struct fields. | ||||||
|  | // | ||||||
|  | // src is the source map for decoding, we use it here to see if those required fields are included in src | ||||||
|  | func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { | ||||||
|  | 	m, errs := d.findRequiredFields(t, "", "") | ||||||
|  | 	for key, fields := range m { | ||||||
|  | 		if isEmptyFields(fields, src) { | ||||||
|  | 			errs[key] = EmptyFieldError{Key: key} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // findRequiredFields recursively searches the struct type t for required fields. | ||||||
|  | // | ||||||
|  | // canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation | ||||||
|  | // for nested struct fields. canonicalPrefix is a complete path which never omits | ||||||
|  | // any embedded struct fields. searchPrefix is a user-friendly path which may omit | ||||||
|  | // some embedded struct fields to point promoted fields. | ||||||
|  | func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { | ||||||
|  | 	struc := d.cache.get(t) | ||||||
|  | 	if struc == nil { | ||||||
|  | 		// unexpect, cache.get never return nil | ||||||
|  | 		return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m := map[string][]fieldWithPrefix{} | ||||||
|  | 	errs := MultiError{} | ||||||
|  | 	for _, f := range struc.fields { | ||||||
|  | 		if f.typ.Kind() == reflect.Struct { | ||||||
|  | 			fcprefix := canonicalPrefix + f.canonicalAlias + "." | ||||||
|  | 			for _, fspath := range f.paths(searchPrefix) { | ||||||
|  | 				fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") | ||||||
|  | 				for key, fields := range fm { | ||||||
|  | 					m[key] = append(m[key], fields...) | ||||||
|  | 				} | ||||||
|  | 				errs.merge(ferrs) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if f.isRequired { | ||||||
|  | 			key := canonicalPrefix + f.canonicalAlias | ||||||
|  | 			m[key] = append(m[key], fieldWithPrefix{ | ||||||
|  | 				fieldInfo: f, | ||||||
|  | 				prefix:    searchPrefix, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return m, errs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type fieldWithPrefix struct { | ||||||
|  | 	*fieldInfo | ||||||
|  | 	prefix string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // isEmptyFields returns true if all of specified fields are empty. | ||||||
|  | func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { | ||||||
|  | 	for _, f := range fields { | ||||||
|  | 		for _, path := range f.paths(f.prefix) { | ||||||
|  | 			v, ok := src[path] | ||||||
|  | 			if ok && !isEmpty(f.typ, v) { | ||||||
|  | 				return false | ||||||
|  | 			} | ||||||
|  | 			for key := range src { | ||||||
|  | 				// issue references: | ||||||
|  | 				// https://github.com/gofiber/fiber/issues/1414 | ||||||
|  | 				// https://github.com/gorilla/schema/issues/176 | ||||||
|  | 				nested := strings.IndexByte(key, '.') != -1 | ||||||
|  |  | ||||||
|  | 				// for non required nested structs | ||||||
|  | 				c1 := strings.HasSuffix(f.prefix, ".") && key == path | ||||||
|  |  | ||||||
|  | 				// for required nested structs | ||||||
|  | 				c2 := f.prefix == "" && nested && strings.HasPrefix(key, path) | ||||||
|  |  | ||||||
|  | 				// for non nested fields | ||||||
|  | 				c3 := f.prefix == "" && !nested && key == path | ||||||
|  | 				if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) { | ||||||
|  | 					return false | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // isEmpty returns true if value is empty for specific type | ||||||
|  | func isEmpty(t reflect.Type, value []string) bool { | ||||||
|  | 	if len(value) == 0 { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	switch t.Kind() { | ||||||
|  | 	case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: | ||||||
|  | 		return len(value[0]) == 0 | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // decode fills a struct field using a parsed path. | ||||||
|  | func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { | ||||||
|  | 	// Get the field walking the struct fields by index. | ||||||
|  | 	for _, name := range parts[0].path { | ||||||
|  | 		if v.Type().Kind() == reflect.Ptr { | ||||||
|  | 			if v.IsNil() { | ||||||
|  | 				v.Set(reflect.New(v.Type().Elem())) | ||||||
|  | 			} | ||||||
|  | 			v = v.Elem() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// alloc embedded structs | ||||||
|  | 		if v.Type().Kind() == reflect.Struct { | ||||||
|  | 			for i := 0; i < v.NumField(); i++ { | ||||||
|  | 				field := v.Field(i) | ||||||
|  | 				if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { | ||||||
|  | 					field.Set(reflect.New(field.Type().Elem())) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		v = v.FieldByName(name) | ||||||
|  | 	} | ||||||
|  | 	// Don't even bother for unexported fields. | ||||||
|  | 	if !v.CanSet() { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Dereference if needed. | ||||||
|  | 	t := v.Type() | ||||||
|  | 	if t.Kind() == reflect.Ptr { | ||||||
|  | 		t = t.Elem() | ||||||
|  | 		if v.IsNil() { | ||||||
|  | 			v.Set(reflect.New(t)) | ||||||
|  | 		} | ||||||
|  | 		v = v.Elem() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Slice of structs. Let's go recursive. | ||||||
|  | 	if len(parts) > 1 { | ||||||
|  | 		idx := parts[0].index | ||||||
|  | 		if v.IsNil() || v.Len() < idx+1 { | ||||||
|  | 			value := reflect.MakeSlice(t, idx+1, idx+1) | ||||||
|  | 			if v.Len() < idx+1 { | ||||||
|  | 				// Resize it. | ||||||
|  | 				reflect.Copy(value, v) | ||||||
|  | 			} | ||||||
|  | 			v.Set(value) | ||||||
|  | 		} | ||||||
|  | 		return d.decode(v.Index(idx), path, parts[1:], values) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get the converter early in case there is one for a slice type. | ||||||
|  | 	conv := d.cache.converter(t) | ||||||
|  | 	m := isTextUnmarshaler(v) | ||||||
|  | 	if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { | ||||||
|  | 		var items []reflect.Value | ||||||
|  | 		elemT := t.Elem() | ||||||
|  | 		isPtrElem := elemT.Kind() == reflect.Ptr | ||||||
|  | 		if isPtrElem { | ||||||
|  | 			elemT = elemT.Elem() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Try to get a converter for the element type. | ||||||
|  | 		conv := d.cache.converter(elemT) | ||||||
|  | 		if conv == nil { | ||||||
|  | 			conv = builtinConverters[elemT.Kind()] | ||||||
|  | 			if conv == nil { | ||||||
|  | 				// As we are not dealing with slice of structs here, we don't need to check if the type | ||||||
|  | 				// implements TextUnmarshaler interface | ||||||
|  | 				return fmt.Errorf("schema: converter not found for %v", elemT) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for key, value := range values { | ||||||
|  | 			if value == "" { | ||||||
|  | 				if d.zeroEmpty { | ||||||
|  | 					items = append(items, reflect.Zero(elemT)) | ||||||
|  | 				} | ||||||
|  | 			} else if m.IsValid { | ||||||
|  | 				u := reflect.New(elemT) | ||||||
|  | 				if m.IsSliceElementPtr { | ||||||
|  | 					u = reflect.New(reflect.PtrTo(elemT).Elem()) | ||||||
|  | 				} | ||||||
|  | 				if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { | ||||||
|  | 					return ConversionError{ | ||||||
|  | 						Key:   path, | ||||||
|  | 						Type:  t, | ||||||
|  | 						Index: key, | ||||||
|  | 						Err:   err, | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if m.IsSliceElementPtr { | ||||||
|  | 					items = append(items, u.Elem().Addr()) | ||||||
|  | 				} else if u.Kind() == reflect.Ptr { | ||||||
|  | 					items = append(items, u.Elem()) | ||||||
|  | 				} else { | ||||||
|  | 					items = append(items, u) | ||||||
|  | 				} | ||||||
|  | 			} else if item := conv(value); item.IsValid() { | ||||||
|  | 				if isPtrElem { | ||||||
|  | 					ptr := reflect.New(elemT) | ||||||
|  | 					ptr.Elem().Set(item) | ||||||
|  | 					item = ptr | ||||||
|  | 				} | ||||||
|  | 				if item.Type() != elemT && !isPtrElem { | ||||||
|  | 					item = item.Convert(elemT) | ||||||
|  | 				} | ||||||
|  | 				items = append(items, item) | ||||||
|  | 			} else { | ||||||
|  | 				if strings.Contains(value, ",") { | ||||||
|  | 					values := strings.Split(value, ",") | ||||||
|  | 					for _, value := range values { | ||||||
|  | 						if value == "" { | ||||||
|  | 							if d.zeroEmpty { | ||||||
|  | 								items = append(items, reflect.Zero(elemT)) | ||||||
|  | 							} | ||||||
|  | 						} else if item := conv(value); item.IsValid() { | ||||||
|  | 							if isPtrElem { | ||||||
|  | 								ptr := reflect.New(elemT) | ||||||
|  | 								ptr.Elem().Set(item) | ||||||
|  | 								item = ptr | ||||||
|  | 							} | ||||||
|  | 							if item.Type() != elemT && !isPtrElem { | ||||||
|  | 								item = item.Convert(elemT) | ||||||
|  | 							} | ||||||
|  | 							items = append(items, item) | ||||||
|  | 						} else { | ||||||
|  | 							return ConversionError{ | ||||||
|  | 								Key:   path, | ||||||
|  | 								Type:  elemT, | ||||||
|  | 								Index: key, | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					return ConversionError{ | ||||||
|  | 						Key:   path, | ||||||
|  | 						Type:  elemT, | ||||||
|  | 						Index: key, | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) | ||||||
|  | 		v.Set(value) | ||||||
|  | 	} else { | ||||||
|  | 		val := "" | ||||||
|  | 		// Use the last value provided if any values were provided | ||||||
|  | 		if len(values) > 0 { | ||||||
|  | 			val = values[len(values)-1] | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if conv != nil { | ||||||
|  | 			if value := conv(val); value.IsValid() { | ||||||
|  | 				v.Set(value.Convert(t)) | ||||||
|  | 			} else { | ||||||
|  | 				return ConversionError{ | ||||||
|  | 					Key:   path, | ||||||
|  | 					Type:  t, | ||||||
|  | 					Index: -1, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else if m.IsValid { | ||||||
|  | 			if m.IsPtr { | ||||||
|  | 				u := reflect.New(v.Type()) | ||||||
|  | 				if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { | ||||||
|  | 					return ConversionError{ | ||||||
|  | 						Key:   path, | ||||||
|  | 						Type:  t, | ||||||
|  | 						Index: -1, | ||||||
|  | 						Err:   err, | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				v.Set(reflect.Indirect(u)) | ||||||
|  | 			} else { | ||||||
|  | 				// If the value implements the encoding.TextUnmarshaler interface | ||||||
|  | 				// apply UnmarshalText as the converter | ||||||
|  | 				if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { | ||||||
|  | 					return ConversionError{ | ||||||
|  | 						Key:   path, | ||||||
|  | 						Type:  t, | ||||||
|  | 						Index: -1, | ||||||
|  | 						Err:   err, | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else if val == "" { | ||||||
|  | 			if d.zeroEmpty { | ||||||
|  | 				v.Set(reflect.Zero(t)) | ||||||
|  | 			} | ||||||
|  | 		} else if conv := builtinConverters[t.Kind()]; conv != nil { | ||||||
|  | 			if value := conv(val); value.IsValid() { | ||||||
|  | 				v.Set(value.Convert(t)) | ||||||
|  | 			} else { | ||||||
|  | 				return ConversionError{ | ||||||
|  | 					Key:   path, | ||||||
|  | 					Type:  t, | ||||||
|  | 					Index: -1, | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return fmt.Errorf("schema: converter not found for %v", t) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isTextUnmarshaler(v reflect.Value) unmarshaler { | ||||||
|  | 	// Create a new unmarshaller instance | ||||||
|  | 	m := unmarshaler{} | ||||||
|  | 	if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { | ||||||
|  | 		return m | ||||||
|  | 	} | ||||||
|  | 	// As the UnmarshalText function should be applied to the pointer of the | ||||||
|  | 	// type, we check that type to see if it implements the necessary | ||||||
|  | 	// method. | ||||||
|  | 	if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { | ||||||
|  | 		m.IsPtr = true | ||||||
|  | 		return m | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// if v is []T or *[]T create new T | ||||||
|  | 	t := v.Type() | ||||||
|  | 	if t.Kind() == reflect.Ptr { | ||||||
|  | 		t = t.Elem() | ||||||
|  | 	} | ||||||
|  | 	if t.Kind() == reflect.Slice { | ||||||
|  | 		// Check if the slice implements encoding.TextUnmarshaller | ||||||
|  | 		if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { | ||||||
|  | 			return m | ||||||
|  | 		} | ||||||
|  | 		// If t is a pointer slice, check if its elements implement | ||||||
|  | 		// encoding.TextUnmarshaler | ||||||
|  | 		m.IsSliceElement = true | ||||||
|  | 		if t = t.Elem(); t.Kind() == reflect.Ptr { | ||||||
|  | 			t = reflect.PtrTo(t.Elem()) | ||||||
|  | 			v = reflect.Zero(t) | ||||||
|  | 			m.IsSliceElementPtr = true | ||||||
|  | 			m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) | ||||||
|  | 			return m | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	v = reflect.New(t) | ||||||
|  | 	m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TextUnmarshaler helpers ---------------------------------------------------- | ||||||
|  | // unmarshaller contains information about a TextUnmarshaler type | ||||||
|  | type unmarshaler struct { | ||||||
|  | 	Unmarshaler encoding.TextUnmarshaler | ||||||
|  | 	// IsValid indicates whether the resolved type indicated by the other | ||||||
|  | 	// flags implements the encoding.TextUnmarshaler interface. | ||||||
|  | 	IsValid bool | ||||||
|  | 	// IsPtr indicates that the resolved type is the pointer of the original | ||||||
|  | 	// type. | ||||||
|  | 	IsPtr bool | ||||||
|  | 	// IsSliceElement indicates that the resolved type is a slice element of | ||||||
|  | 	// the original type. | ||||||
|  | 	IsSliceElement bool | ||||||
|  | 	// IsSliceElementPtr indicates that the resolved type is a pointer to a | ||||||
|  | 	// slice element of the original type. | ||||||
|  | 	IsSliceElementPtr bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Errors --------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | // ConversionError stores information about a failed conversion. | ||||||
|  | type ConversionError struct { | ||||||
|  | 	Key   string       // key from the source map. | ||||||
|  | 	Type  reflect.Type // expected type of elem | ||||||
|  | 	Index int          // index for multi-value fields; -1 for single-value fields. | ||||||
|  | 	Err   error        // low-level error (when it exists) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e ConversionError) Error() string { | ||||||
|  | 	var output string | ||||||
|  |  | ||||||
|  | 	if e.Index < 0 { | ||||||
|  | 		output = fmt.Sprintf("schema: error converting value for %q", e.Key) | ||||||
|  | 	} else { | ||||||
|  | 		output = fmt.Sprintf("schema: error converting value for index %d of %q", | ||||||
|  | 			e.Index, e.Key) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if e.Err != nil { | ||||||
|  | 		output = fmt.Sprintf("%s. Details: %s", output, e.Err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return output | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnknownKeyError stores information about an unknown key in the source map. | ||||||
|  | type UnknownKeyError struct { | ||||||
|  | 	Key string // key from the source map. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e UnknownKeyError) Error() string { | ||||||
|  | 	return fmt.Sprintf("schema: invalid path %q", e.Key) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // EmptyFieldError stores information about an empty required field. | ||||||
|  | type EmptyFieldError struct { | ||||||
|  | 	Key string // required key in the source map. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e EmptyFieldError) Error() string { | ||||||
|  | 	return fmt.Sprintf("%v is empty", e.Key) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MultiError stores multiple decoding errors. | ||||||
|  | // | ||||||
|  | // Borrowed from the App Engine SDK. | ||||||
|  | type MultiError map[string]error | ||||||
|  |  | ||||||
|  | func (e MultiError) Error() string { | ||||||
|  | 	s := "" | ||||||
|  | 	for _, err := range e { | ||||||
|  | 		s = err.Error() | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  | 	switch len(e) { | ||||||
|  | 	case 0: | ||||||
|  | 		return "(0 errors)" | ||||||
|  | 	case 1: | ||||||
|  | 		return s | ||||||
|  | 	case 2: | ||||||
|  | 		return s + " (and 1 other error)" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e MultiError) merge(errors MultiError) { | ||||||
|  | 	for key, err := range errors { | ||||||
|  | 		if e[key] == nil { | ||||||
|  | 			e[key] = err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										148
									
								
								internal/schema/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								internal/schema/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | // Copyright 2012 The Gorilla Authors. All rights reserved. | ||||||
|  | // Use of this source code is governed by a BSD-style | ||||||
|  | // license that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | Package gorilla/schema fills a struct with form values. | ||||||
|  |  | ||||||
|  | The basic usage is really simple. Given this struct: | ||||||
|  |  | ||||||
|  | 	type Person struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Phone string | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | ...we can fill it passing a map to the Decode() function: | ||||||
|  |  | ||||||
|  | 	values := map[string][]string{ | ||||||
|  | 		"Name":  {"John"}, | ||||||
|  | 		"Phone": {"999-999-999"}, | ||||||
|  | 	} | ||||||
|  | 	person := new(Person) | ||||||
|  | 	decoder := schema.NewDecoder() | ||||||
|  | 	decoder.Decode(person, values) | ||||||
|  |  | ||||||
|  | This is just a simple example and it doesn't make a lot of sense to create | ||||||
|  | the map manually. Typically it will come from a http.Request object and | ||||||
|  | will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: | ||||||
|  |  | ||||||
|  | 	func MyHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		err := r.ParseForm() | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			// Handle error | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		decoder := schema.NewDecoder() | ||||||
|  | 		// r.PostForm is a map of our POST form values | ||||||
|  | 		err := decoder.Decode(person, r.PostForm) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			// Handle error | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Do something with person.Name or person.Phone | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | Note: it is a good idea to set a Decoder instance as a package global, | ||||||
|  | because it caches meta-data about structs, and an instance can be shared safely: | ||||||
|  |  | ||||||
|  | 	var decoder = schema.NewDecoder() | ||||||
|  |  | ||||||
|  | To define custom names for fields, use a struct tag "schema". To not populate | ||||||
|  | certain fields, use a dash for the name and it will be ignored: | ||||||
|  |  | ||||||
|  | 	type Person struct { | ||||||
|  | 		Name  string `schema:"name"`  // custom name | ||||||
|  | 		Phone string `schema:"phone"` // custom name | ||||||
|  | 		Admin bool   `schema:"-"`     // this field is never set | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | The supported field types in the destination struct are: | ||||||
|  |  | ||||||
|  |   - bool | ||||||
|  |   - float variants (float32, float64) | ||||||
|  |   - int variants (int, int8, int16, int32, int64) | ||||||
|  |   - string | ||||||
|  |   - uint variants (uint, uint8, uint16, uint32, uint64) | ||||||
|  |   - struct | ||||||
|  |   - a pointer to one of the above types | ||||||
|  |   - a slice or a pointer to a slice of one of the above types | ||||||
|  |  | ||||||
|  | Non-supported types are simply ignored, however custom types can be registered | ||||||
|  | to be converted. | ||||||
|  |  | ||||||
|  | To fill nested structs, keys must use a dotted notation as the "path" for the | ||||||
|  | field. So for example, to fill the struct Person below: | ||||||
|  |  | ||||||
|  | 	type Phone struct { | ||||||
|  | 		Label  string | ||||||
|  | 		Number string | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	type Person struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Phone Phone | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | ...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". | ||||||
|  | This means that an HTML form to fill a Person struct must look like this: | ||||||
|  |  | ||||||
|  | 	<form> | ||||||
|  | 		<input type="text" name="Name"> | ||||||
|  | 		<input type="text" name="Phone.Label"> | ||||||
|  | 		<input type="text" name="Phone.Number"> | ||||||
|  | 	</form> | ||||||
|  |  | ||||||
|  | Single values are filled using the first value for a key from the source map. | ||||||
|  | Slices are filled using all values for a key from the source map. So to fill | ||||||
|  | a Person with multiple Phone values, like: | ||||||
|  |  | ||||||
|  | 	type Person struct { | ||||||
|  | 		Name   string | ||||||
|  | 		Phones []Phone | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | ...an HTML form that accepts three Phone values would look like this: | ||||||
|  |  | ||||||
|  | 	<form> | ||||||
|  | 		<input type="text" name="Name"> | ||||||
|  | 		<input type="text" name="Phones.0.Label"> | ||||||
|  | 		<input type="text" name="Phones.0.Number"> | ||||||
|  | 		<input type="text" name="Phones.1.Label"> | ||||||
|  | 		<input type="text" name="Phones.1.Number"> | ||||||
|  | 		<input type="text" name="Phones.2.Label"> | ||||||
|  | 		<input type="text" name="Phones.2.Number"> | ||||||
|  | 	</form> | ||||||
|  |  | ||||||
|  | Notice that only for slices of structs the slice index is required. | ||||||
|  | This is needed for disambiguation: if the nested struct also had a slice | ||||||
|  | field, we could not translate multiple values to it if we did not use an | ||||||
|  | index for the parent struct. | ||||||
|  |  | ||||||
|  | There's also the possibility to create a custom type that implements the | ||||||
|  | TextUnmarshaler interface, and in this case there's no need to register | ||||||
|  | a converter, like: | ||||||
|  |  | ||||||
|  | 	type Person struct { | ||||||
|  | 	  Emails []Email | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	type Email struct { | ||||||
|  | 	  *mail.Address | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	func (e *Email) UnmarshalText(text []byte) (err error) { | ||||||
|  | 		e.Address, err = mail.ParseAddress(string(text)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | ...an HTML form that accepts three Email values would look like this: | ||||||
|  |  | ||||||
|  | 	<form> | ||||||
|  | 		<input type="email" name="Emails.0"> | ||||||
|  | 		<input type="email" name="Emails.1"> | ||||||
|  | 		<input type="email" name="Emails.2"> | ||||||
|  | 	</form> | ||||||
|  | */ | ||||||
|  | package schema | ||||||
							
								
								
									
										202
									
								
								internal/schema/encoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								internal/schema/encoder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | |||||||
|  | package schema | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strconv" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type encoderFunc func(reflect.Value) string | ||||||
|  |  | ||||||
|  | // Encoder encodes values from a struct into url.Values. | ||||||
|  | type Encoder struct { | ||||||
|  | 	cache  *cache | ||||||
|  | 	regenc map[reflect.Type]encoderFunc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewEncoder returns a new Encoder with defaults. | ||||||
|  | func NewEncoder() *Encoder { | ||||||
|  | 	return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Encode encodes a struct into map[string][]string. | ||||||
|  | // | ||||||
|  | // Intended for use with url.Values. | ||||||
|  | func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { | ||||||
|  | 	v := reflect.ValueOf(src) | ||||||
|  |  | ||||||
|  | 	return e.encode(v, dst) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RegisterEncoder registers a converter for encoding a custom type. | ||||||
|  | func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { | ||||||
|  | 	e.regenc[reflect.TypeOf(value)] = encoder | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetAliasTag changes the tag used to locate custom field aliases. | ||||||
|  | // The default tag is "schema". | ||||||
|  | func (e *Encoder) SetAliasTag(tag string) { | ||||||
|  | 	e.cache.tag = tag | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // isValidStructPointer test if input value is a valid struct pointer. | ||||||
|  | func isValidStructPointer(v reflect.Value) bool { | ||||||
|  | 	return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isZero(v reflect.Value) bool { | ||||||
|  | 	switch v.Kind() { | ||||||
|  | 	case reflect.Func: | ||||||
|  | 	case reflect.Map, reflect.Slice: | ||||||
|  | 		return v.IsNil() || v.Len() == 0 | ||||||
|  | 	case reflect.Array: | ||||||
|  | 		z := true | ||||||
|  | 		for i := 0; i < v.Len(); i++ { | ||||||
|  | 			z = z && isZero(v.Index(i)) | ||||||
|  | 		} | ||||||
|  | 		return z | ||||||
|  | 	case reflect.Struct: | ||||||
|  | 		type zero interface { | ||||||
|  | 			IsZero() bool | ||||||
|  | 		} | ||||||
|  | 		if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { | ||||||
|  | 			iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] | ||||||
|  | 			return iz.Interface().(bool) | ||||||
|  | 		} | ||||||
|  | 		z := true | ||||||
|  | 		for i := 0; i < v.NumField(); i++ { | ||||||
|  | 			z = z && isZero(v.Field(i)) | ||||||
|  | 		} | ||||||
|  | 		return z | ||||||
|  | 	} | ||||||
|  | 	// Compare other types directly: | ||||||
|  | 	z := reflect.Zero(v.Type()) | ||||||
|  | 	return v.Interface() == z.Interface() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { | ||||||
|  | 	if v.Kind() == reflect.Ptr { | ||||||
|  | 		v = v.Elem() | ||||||
|  | 	} | ||||||
|  | 	if v.Kind() != reflect.Struct { | ||||||
|  | 		return errors.New("schema: interface must be a struct") | ||||||
|  | 	} | ||||||
|  | 	t := v.Type() | ||||||
|  |  | ||||||
|  | 	errors := MultiError{} | ||||||
|  |  | ||||||
|  | 	for i := 0; i < v.NumField(); i++ { | ||||||
|  | 		name, opts := fieldAlias(t.Field(i), e.cache.tag) | ||||||
|  | 		if name == "-" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Encode struct pointer types if the field is a valid pointer and a struct. | ||||||
|  | 		if isValidStructPointer(v.Field(i)) { | ||||||
|  | 			_ = e.encode(v.Field(i).Elem(), dst) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		encFunc := typeEncoder(v.Field(i).Type(), e.regenc) | ||||||
|  |  | ||||||
|  | 		// Encode non-slice types and custom implementations immediately. | ||||||
|  | 		if encFunc != nil { | ||||||
|  | 			value := encFunc(v.Field(i)) | ||||||
|  | 			if opts.Contains("omitempty") && isZero(v.Field(i)) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			dst[name] = append(dst[name], value) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if v.Field(i).Type().Kind() == reflect.Struct { | ||||||
|  | 			_ = e.encode(v.Field(i), dst) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if v.Field(i).Type().Kind() == reflect.Slice { | ||||||
|  | 			encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if encFunc == nil { | ||||||
|  | 			errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Encode a slice. | ||||||
|  | 		if v.Field(i).Len() == 0 && opts.Contains("omitempty") { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		dst[name] = []string{} | ||||||
|  | 		for j := 0; j < v.Field(i).Len(); j++ { | ||||||
|  | 			dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(errors) > 0 { | ||||||
|  | 		return errors | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { | ||||||
|  | 	if f, ok := reg[t]; ok { | ||||||
|  | 		return f | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch t.Kind() { | ||||||
|  | 	case reflect.Bool: | ||||||
|  | 		return encodeBool | ||||||
|  | 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | ||||||
|  | 		return encodeInt | ||||||
|  | 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | ||||||
|  | 		return encodeUint | ||||||
|  | 	case reflect.Float32: | ||||||
|  | 		return encodeFloat32 | ||||||
|  | 	case reflect.Float64: | ||||||
|  | 		return encodeFloat64 | ||||||
|  | 	case reflect.Ptr: | ||||||
|  | 		f := typeEncoder(t.Elem(), reg) | ||||||
|  | 		return func(v reflect.Value) string { | ||||||
|  | 			if v.IsNil() { | ||||||
|  | 				return "null" | ||||||
|  | 			} | ||||||
|  | 			return f(v.Elem()) | ||||||
|  | 		} | ||||||
|  | 	case reflect.String: | ||||||
|  | 		return encodeString | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeBool(v reflect.Value) string { | ||||||
|  | 	return strconv.FormatBool(v.Bool()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeInt(v reflect.Value) string { | ||||||
|  | 	return strconv.FormatInt(int64(v.Int()), 10) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeUint(v reflect.Value) string { | ||||||
|  | 	return strconv.FormatUint(uint64(v.Uint()), 10) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeFloat(v reflect.Value, bits int) string { | ||||||
|  | 	return strconv.FormatFloat(v.Float(), 'f', 6, bits) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeFloat32(v reflect.Value) string { | ||||||
|  | 	return encodeFloat(v, 32) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeFloat64(v reflect.Value) string { | ||||||
|  | 	return encodeFloat(v, 64) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encodeString(v reflect.Value) string { | ||||||
|  | 	return v.String() | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								middleware.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"runtime/debug" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func NewRecover(enableStackTrace bool) HandlerFunc { | ||||||
|  | 	return func(c *Ctx) error { | ||||||
|  | 		defer func() { | ||||||
|  | 			if r := recover(); r != nil { | ||||||
|  | 				if enableStackTrace { | ||||||
|  | 					os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n%s\n", r, debug.Stack())) | ||||||
|  | 				} else { | ||||||
|  | 					os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n", r)) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  |  | ||||||
|  | 		return c.Next() | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								nf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								nf.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	banner = "  _  _     _     ___                 _ \n | \\| |___| |_  | __|__ _  _ _ _  __| |\n | .` / _ \\  _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n " | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Map map[string]interface{} | ||||||
|  |  | ||||||
|  | type Config struct { | ||||||
|  | 	// Default: 4 * 1024 * 1024 | ||||||
|  | 	BodyLimit      int64 `json:"-"` | ||||||
|  | 	DisableBanner  bool  `json:"-"` | ||||||
|  | 	DisableLogger  bool  `json:"-"` | ||||||
|  | 	DisableRecover bool  `json:"-"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	defaultConfig = &Config{ | ||||||
|  | 		BodyLimit: 4 * 1024 * 1024, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func New(config ...Config) *App { | ||||||
|  | 	app := &App{ | ||||||
|  | 		router: newRouter(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(config) > 0 { | ||||||
|  | 		app.config = &config[0] | ||||||
|  | 		if app.config.BodyLimit == 0 { | ||||||
|  | 			app.config.BodyLimit = defaultConfig.BodyLimit | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		app.config = defaultConfig | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	app.RouterGroup = &RouterGroup{app: app} | ||||||
|  | 	app.groups = []*RouterGroup{app.RouterGroup} | ||||||
|  |  | ||||||
|  | 	if !app.config.DisableRecover { | ||||||
|  | 		app.Use(NewRecover(true)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return app | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								resp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								resp.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Ctx) Status(code int) *Ctx { | ||||||
|  | 	c.StatusCode = code | ||||||
|  | 	c.Writer.WriteHeader(code) | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) SetHeader(key string, value string) { | ||||||
|  | 	c.Writer.Header().Set(key, value) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) SendString(data string) error { | ||||||
|  | 	c.SetHeader("Content-Type", "text/plain") | ||||||
|  | 	_, err := c.Write([]byte(data)) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Writef(format string, values ...interface{}) (int, error) { | ||||||
|  | 	c.SetHeader("Content-Type", "text/plain") | ||||||
|  | 	return c.Writer.Write([]byte(fmt.Sprintf(format, values...))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) JSON(data interface{}) error { | ||||||
|  | 	c.SetHeader("Content-Type", "application/json") | ||||||
|  |  | ||||||
|  | 	encoder := json.NewEncoder(c.Writer) | ||||||
|  |  | ||||||
|  | 	if err := encoder.Encode(data); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) Write(data []byte) (int, error) { | ||||||
|  | 	return c.Writer.Write(data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Ctx) HTML(html string) error { | ||||||
|  | 	c.SetHeader("Content-Type", "text/html") | ||||||
|  | 	_, err := c.Writer.Write([]byte(html)) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
							
								
								
									
										99
									
								
								router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								router.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import "strings" | ||||||
|  |  | ||||||
|  | type router struct { | ||||||
|  | 	roots    map[string]*_node | ||||||
|  | 	handlers map[string][]HandlerFunc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newRouter() *router { | ||||||
|  | 	return &router{ | ||||||
|  | 		roots:    make(map[string]*_node), | ||||||
|  | 		handlers: make(map[string][]HandlerFunc), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Only one * is allowed | ||||||
|  | func parsePattern(pattern string) []string { | ||||||
|  | 	vs := strings.Split(pattern, "/") | ||||||
|  |  | ||||||
|  | 	parts := make([]string, 0) | ||||||
|  | 	for _, item := range vs { | ||||||
|  | 		if item != "" { | ||||||
|  | 			parts = append(parts, item) | ||||||
|  | 			if item[0] == '*' { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return parts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *router) addRoute(method string, pattern string, handlers ...HandlerFunc) { | ||||||
|  | 	parts := parsePattern(pattern) | ||||||
|  |  | ||||||
|  | 	key := method + "-" + pattern | ||||||
|  | 	_, ok := r.roots[method] | ||||||
|  | 	if !ok { | ||||||
|  | 		r.roots[method] = &_node{} | ||||||
|  | 	} | ||||||
|  | 	r.roots[method].insert(pattern, parts, 0) | ||||||
|  | 	r.handlers[key] = handlers | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *router) getRoute(method string, path string) (*_node, map[string]string) { | ||||||
|  | 	searchParts := parsePattern(path) | ||||||
|  | 	params := make(map[string]string) | ||||||
|  | 	root, ok := r.roots[method] | ||||||
|  |  | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	n := root.search(searchParts, 0) | ||||||
|  |  | ||||||
|  | 	if n != nil { | ||||||
|  | 		parts := parsePattern(n.pattern) | ||||||
|  | 		for index, part := range parts { | ||||||
|  | 			if part[0] == ':' { | ||||||
|  | 				params[part[1:]] = searchParts[index] | ||||||
|  | 			} | ||||||
|  | 			if part[0] == '*' && len(part) > 1 { | ||||||
|  | 				params[part[1:]] = strings.Join(searchParts[index:], "/") | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return n, params | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *router) getRoutes(method string) []*_node { | ||||||
|  | 	root, ok := r.roots[method] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	nodes := make([]*_node, 0) | ||||||
|  | 	root.travel(&nodes) | ||||||
|  | 	return nodes | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *router) handle(c *Ctx) error { | ||||||
|  | 	if err := c.verify(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	node, params := r.getRoute(c.Method, c.path) | ||||||
|  | 	if node != nil { | ||||||
|  | 		c.params = params | ||||||
|  | 		key := c.Method + "-" + node.pattern | ||||||
|  | 		c.handlers = append(c.handlers, r.handlers[key]...) | ||||||
|  | 	} else { | ||||||
|  | 		_, err := c.Writef("404 NOT FOUND: %s\n", c.path) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.Next() | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								tree.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								tree.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type _node struct { | ||||||
|  | 	pattern  string | ||||||
|  | 	part     string | ||||||
|  | 	children []*_node | ||||||
|  | 	isWild   bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *_node) insert(pattern string, parts []string, height int) { | ||||||
|  | 	if len(parts) == height { | ||||||
|  | 		n.pattern = pattern | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	part := parts[height] | ||||||
|  | 	child := n.matchChild(part) | ||||||
|  | 	if child == nil { | ||||||
|  | 		child = &_node{part: part, isWild: part[0] == ':' || part[0] == '*'} | ||||||
|  | 		n.children = append(n.children, child) | ||||||
|  | 	} | ||||||
|  | 	child.insert(pattern, parts, height+1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *_node) search(parts []string, height int) *_node { | ||||||
|  | 	if len(parts) == height || strings.HasPrefix(n.part, "*") { | ||||||
|  | 		if n.pattern == "" { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		return n | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	part := parts[height] | ||||||
|  | 	children := n.matchChildren(part) | ||||||
|  |  | ||||||
|  | 	for _, child := range children { | ||||||
|  | 		result := child.search(parts, height+1) | ||||||
|  | 		if result != nil { | ||||||
|  | 			return result | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *_node) travel(list *([]*_node)) { | ||||||
|  | 	if n.pattern != "" { | ||||||
|  | 		*list = append(*list, n) | ||||||
|  | 	} | ||||||
|  | 	for _, child := range n.children { | ||||||
|  | 		child.travel(list) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *_node) matchChild(part string) *_node { | ||||||
|  | 	for _, child := range n.children { | ||||||
|  | 		if child.part == part || child.isWild { | ||||||
|  | 			return child | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n *_node) matchChildren(part string) []*_node { | ||||||
|  | 	nodes := make([]*_node, 0) | ||||||
|  | 	for _, child := range n.children { | ||||||
|  | 		if child.part == part || child.isWild { | ||||||
|  | 			nodes = append(nodes, child) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nodes | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | package nf | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/loveuer/nf/internal/schema" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	MIMETextXML         = "text/xml" | ||||||
|  | 	MIMETextHTML        = "text/html" | ||||||
|  | 	MIMETextPlain       = "text/plain" | ||||||
|  | 	MIMETextJavaScript  = "text/javascript" | ||||||
|  | 	MIMEApplicationXML  = "application/xml" | ||||||
|  | 	MIMEApplicationJSON = "application/json" | ||||||
|  | 	// Deprecated: use MIMETextJavaScript instead | ||||||
|  | 	MIMEApplicationJavaScript = "application/javascript" | ||||||
|  | 	MIMEApplicationForm       = "application/x-www-form-urlencoded" | ||||||
|  | 	MIMEOctetStream           = "application/octet-stream" | ||||||
|  | 	MIMEMultipartForm         = "multipart/form-data" | ||||||
|  |  | ||||||
|  | 	MIMETextXMLCharsetUTF8         = "text/xml; charset=utf-8" | ||||||
|  | 	MIMETextHTMLCharsetUTF8        = "text/html; charset=utf-8" | ||||||
|  | 	MIMETextPlainCharsetUTF8       = "text/plain; charset=utf-8" | ||||||
|  | 	MIMETextJavaScriptCharsetUTF8  = "text/javascript; charset=utf-8" | ||||||
|  | 	MIMEApplicationXMLCharsetUTF8  = "application/xml; charset=utf-8" | ||||||
|  | 	MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" | ||||||
|  | 	// Deprecated: use MIMETextJavaScriptCharsetUTF8 instead | ||||||
|  | 	MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func verifyHandlers(path string, handlers ...HandlerFunc) { | ||||||
|  | 	if len(handlers) == 0 { | ||||||
|  | 		panic(fmt.Sprintf("missing handler in route: %s", path)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, handler := range handlers { | ||||||
|  | 		if handler == nil { | ||||||
|  | 			panic(fmt.Sprintf("nil handler found in route: %s", path)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // parseVendorSpecificContentType check if content type is vendor specific and | ||||||
|  | // if it is parsable to any known types. If it's not vendor specific then returns | ||||||
|  | // the original content type. | ||||||
|  | func parseVendorSpecificContentType(cType string) string { | ||||||
|  | 	plusIndex := strings.Index(cType, "+") | ||||||
|  |  | ||||||
|  | 	if plusIndex == -1 { | ||||||
|  | 		return cType | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var parsableType string | ||||||
|  | 	if semiColonIndex := strings.Index(cType, ";"); semiColonIndex == -1 { | ||||||
|  | 		parsableType = cType[plusIndex+1:] | ||||||
|  | 	} else if plusIndex < semiColonIndex { | ||||||
|  | 		parsableType = cType[plusIndex+1 : semiColonIndex] | ||||||
|  | 	} else { | ||||||
|  | 		return cType[:semiColonIndex] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	slashIndex := strings.Index(cType, "/") | ||||||
|  |  | ||||||
|  | 	if slashIndex == -1 { | ||||||
|  | 		return cType | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return cType[0:slashIndex+1] + parsableType | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseToStruct(aliasTag string, out interface{}, data map[string][]string) error { | ||||||
|  | 	schemaDecoder := schema.NewDecoder() | ||||||
|  | 	schemaDecoder.SetAliasTag(aliasTag) | ||||||
|  |  | ||||||
|  | 	if err := schemaDecoder.Decode(out, data); err != nil { | ||||||
|  | 		return fmt.Errorf("failed to decode: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								xtest/basic/basic.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								xtest/basic/basic.http
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | ### basic - get | ||||||
|  | GET http://127.0.0.1/hello/nf | ||||||
							
								
								
									
										17
									
								
								xtest/basic/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								xtest/basic/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/loveuer/nf" | ||||||
|  | 	"log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	app := nf.New() | ||||||
|  |  | ||||||
|  | 	app.Get("/hello/:name", func(c *nf.Ctx) error { | ||||||
|  | 		name := c.Param("name") | ||||||
|  | 		return c.JSON(nf.Map{"status": 200, "data": "hello, " + name}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	log.Fatal(app.Run("0.0.0.0:80")) | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								xtest/bodyLimit/body_limit.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								xtest/bodyLimit/body_limit.http
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | ### body_limit | ||||||
|  | POST http://127.0.0.1/data | ||||||
|  | Content-Type: application/json | ||||||
|  |  | ||||||
|  | { | ||||||
|  |   "name": "zyp", | ||||||
|  |   "age": 19, | ||||||
|  |   "likes": ["2233"] | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								xtest/bodyLimit/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								xtest/bodyLimit/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/loveuer/nf" | ||||||
|  | 	"log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	app := nf.New(nf.Config{BodyLimit: -1}) | ||||||
|  |  | ||||||
|  | 	app.Post("/data", func(c *nf.Ctx) error { | ||||||
|  | 		type Req struct { | ||||||
|  | 			Name  string   `json:"name"` | ||||||
|  | 			Age   int      `json:"age"` | ||||||
|  | 			Likes []string `json:"likes"` | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var ( | ||||||
|  | 			err error | ||||||
|  | 			req = new(Req) | ||||||
|  | 		) | ||||||
|  |  | ||||||
|  | 		if err = c.BodyParser(req); err != nil { | ||||||
|  | 			return c.JSON(nf.Map{"status": 400, "err": err.Error()}) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return c.JSON(nf.Map{"status": 200, "data": req}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	app.Post("/url", func(c *nf.Ctx) error { | ||||||
|  | 		type Req struct { | ||||||
|  | 			Name  string   `form:"name"` | ||||||
|  | 			Age   int      `form:"age"` | ||||||
|  | 			Likes []string `form:"likes"` | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var ( | ||||||
|  | 			err error | ||||||
|  | 			req = new(Req) | ||||||
|  | 		) | ||||||
|  |  | ||||||
|  | 		if err = c.BodyParser(req); err != nil { | ||||||
|  | 			return c.JSON(nf.Map{"status": 400, "err": err.Error()}) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return c.JSON(nf.Map{"status": 200, "data": req}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	log.Fatal(app.Run("0.0.0.0:80")) | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								xtest/panic/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								xtest/panic/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/loveuer/nf" | ||||||
|  | 	"log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	app := nf.New(nf.Config{ | ||||||
|  | 		DisableRecover: true, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	app.Get("/hello/:name", func(c *nf.Ctx) error { | ||||||
|  | 		name := c.Param("name") | ||||||
|  |  | ||||||
|  | 		if name == "nf" { | ||||||
|  | 			panic("name is nf") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return c.JSON("nice") | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	log.Fatal(app.Run("0.0.0.0:80")) | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								xtest/panic/panic.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								xtest/panic/panic.http
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | ### panic test | ||||||
|  | GET http://127.0.0.1/hello/nf | ||||||
|  |  | ||||||
|  | ### if covered? | ||||||
|  | GET http://127.0.0.1/hello/world | ||||||
							
								
								
									
										31
									
								
								xtest/queryParser/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								xtest/queryParser/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/loveuer/nf" | ||||||
|  | 	"log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  | 	app := nf.New() | ||||||
|  |  | ||||||
|  | 	app.Get("/hello", func(c *nf.Ctx) error { | ||||||
|  | 		type Req struct { | ||||||
|  | 			Name  string   `query:"name"` | ||||||
|  | 			Age   int      `query:"age"` | ||||||
|  | 			Likes []string `query:"likes"` | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var ( | ||||||
|  | 			err error | ||||||
|  | 			req = new(Req) | ||||||
|  | 		) | ||||||
|  |  | ||||||
|  | 		if err = c.QueryParser(req); err != nil { | ||||||
|  | 			return nf.NewNFError(400, err.Error()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return c.JSON(nf.Map{"status": 200, "data": req}) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	log.Fatal(app.Run("0.0.0.0:80")) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user