mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
One of the best requests https://github.com/kataras/iris/issues/165
This commit is contained in:
parent
7f56cdea8c
commit
9e03a529d6
|
@ -116,7 +116,7 @@ Iris suggests you to use [this](https://github.com/gavv/httpexpect) new suite t
|
||||||
Versioning
|
Versioning
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Current: **v3.0.0-beta**
|
Current: **v3.0.0-beta.1**
|
||||||
> Iris is an active project
|
> Iris is an active project
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ Todo
|
||||||
- [x] Create server & client side (js) library for .on('event', func action(...)) / .emit('event')... (like socket.io but supports only websocket).
|
- [x] Create server & client side (js) library for .on('event', func action(...)) / .emit('event')... (like socket.io but supports only websocket).
|
||||||
- [x] Find and provide support for the most stable template engine and be able to change it via the configuration, keep html/templates support.
|
- [x] Find and provide support for the most stable template engine and be able to change it via the configuration, keep html/templates support.
|
||||||
- [x] Extend, test and publish to the public the [Iris' cmd](https://github.com/kataras/iris/tree/master/iris).
|
- [x] Extend, test and publish to the public the [Iris' cmd](https://github.com/kataras/iris/tree/master/iris).
|
||||||
|
- [x] Route naming and html url func, requested [here](https://github.com/kataras/iris/issues/165).
|
||||||
|
|
||||||
If you're willing to donate click [here](DONATIONS.md)
|
If you're willing to donate click [here](DONATIONS.md)
|
||||||
|
|
||||||
|
|
|
@ -124,8 +124,13 @@ type (
|
||||||
Left string
|
Left string
|
||||||
// Right delimeter, default is }}
|
// Right delimeter, default is }}
|
||||||
Right string
|
Right string
|
||||||
// Funcs for HTMLTemplate html/template
|
// Funcs like html/template
|
||||||
Funcs template.FuncMap
|
Funcs template.FuncMap
|
||||||
|
// Funcs like html/template
|
||||||
|
// the difference from Funcs is that these funcs
|
||||||
|
// can be used inside a layout and can override the predefined (yield,partial...) or add more custom funcs
|
||||||
|
// these can override the Funcs inside no-layout templates also, use it when you know what you're doing
|
||||||
|
LayoutFuncs template.FuncMap
|
||||||
}
|
}
|
||||||
// Pongo the configs for PongoEngine
|
// Pongo the configs for PongoEngine
|
||||||
Pongo struct {
|
Pongo struct {
|
||||||
|
@ -202,7 +207,7 @@ func DefaultTemplate() Template {
|
||||||
ContentType: "text/html",
|
ContentType: "text/html",
|
||||||
Charset: "UTF-8",
|
Charset: "UTF-8",
|
||||||
Layout: "", // currently this is the only config which not working for pongo2 yet but I will find a way
|
Layout: "", // currently this is the only config which not working for pongo2 yet but I will find a way
|
||||||
HTMLTemplate: HTMLTemplate{Left: "{{", Right: "}}", Funcs: template.FuncMap{}},
|
HTMLTemplate: HTMLTemplate{Left: "{{", Right: "}}", Funcs: template.FuncMap{}, LayoutFuncs: template.FuncMap{}},
|
||||||
Pongo: Pongo{Filters: make(map[string]pongo2.FilterFunction, 0), Globals: make(map[string]interface{}, 0)},
|
Pongo: Pongo{Filters: make(map[string]pongo2.FilterFunction, 0), Globals: make(map[string]interface{}, 0)},
|
||||||
Markdown: Markdown{Sanitize: false},
|
Markdown: Markdown{Sanitize: false},
|
||||||
Amber: Amber{Funcs: template.FuncMap{}},
|
Amber: Amber{Funcs: template.FuncMap{}},
|
||||||
|
|
|
@ -12,8 +12,11 @@ var (
|
||||||
ErrHandleAnnotated = errors.New("HandleAnnotated parse: %s")
|
ErrHandleAnnotated = errors.New("HandleAnnotated parse: %s")
|
||||||
// ErrControllerContextNotFound returns an error with message: 'Context *iris.Context could not be found, the Controller won't be registed.'
|
// ErrControllerContextNotFound returns an error with message: 'Context *iris.Context could not be found, the Controller won't be registed.'
|
||||||
ErrControllerContextNotFound = errors.New("Context *iris.Context could not be found, the Controller won't be registed.")
|
ErrControllerContextNotFound = errors.New("Context *iris.Context could not be found, the Controller won't be registed.")
|
||||||
// ErrDirectoryFileNotFound returns an errir with message: 'Directory or file %s couldn't found. Trace: +error trace'
|
// ErrDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace'
|
||||||
ErrDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s")
|
ErrDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s")
|
||||||
|
// ErrRenderRouteNotFound returns an error with message 'Route with name +route_name not found', used inside 'url' template func
|
||||||
|
ErrRenderRouteNotFound = errors.New("Route with name %s not found")
|
||||||
|
|
||||||
// Plugin
|
// Plugin
|
||||||
|
|
||||||
// ErrPluginAlreadyExists returns an error with message: 'Cannot activate the same plugin again, plugin '+plugin name[+plugin description]' is already exists'
|
// ErrPluginAlreadyExists returns an error with message: 'Cannot activate the same plugin again, plugin '+plugin name[+plugin description]' is already exists'
|
||||||
|
|
44
iris.go
44
iris.go
|
@ -1,4 +1,4 @@
|
||||||
// Package iris v3.0.0-beta
|
// Package iris v3.0.0-beta.1
|
||||||
//
|
//
|
||||||
// Note: When 'Station', we mean the Iris type.
|
// Note: When 'Station', we mean the Iris type.
|
||||||
package iris
|
package iris
|
||||||
|
@ -31,15 +31,14 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Version of the iris
|
// Version of the iris
|
||||||
Version = "v3.0.0-beta"
|
Version = "v3.0.0-beta.1"
|
||||||
banner = ` _____ _
|
banner = ` _____ _
|
||||||
|_ _| (_)
|
|_ _| (_)
|
||||||
| | ____ _ ___
|
| | ____ _ ___
|
||||||
| | | __|| |/ __|
|
| | | __|| |/ __|
|
||||||
_| |_| | | |\__ \
|
_| |_| | | |\__ \
|
||||||
|_____|_| |_||___/
|
|_____|_| |_||___/ ` + Version + `
|
||||||
|
`
|
||||||
`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/* for conversion */
|
/* for conversion */
|
||||||
|
@ -118,7 +117,39 @@ func (s *Iris) newContextPool() sync.Pool {
|
||||||
func (s *Iris) initTemplates() {
|
func (s *Iris) initTemplates() {
|
||||||
if s.templates == nil { // because if .Templates() called before server's listen, s.templates != nil when PreListen
|
if s.templates == nil { // because if .Templates() called before server's listen, s.templates != nil when PreListen
|
||||||
// init the templates
|
// init the templates
|
||||||
|
|
||||||
|
// set the custom iris-direct-integration functions, layout and no-layout if HTMLEngine is used
|
||||||
|
if s.config.Render.Template.Engine == config.HTMLEngine {
|
||||||
|
funcs := map[string]interface{}{
|
||||||
|
"url": func(routeName string, args ...interface{}) (string, error) {
|
||||||
|
r := s.RouteByName(routeName)
|
||||||
|
// check if not found
|
||||||
|
if r.GetPath() == "" {
|
||||||
|
return "", ErrRenderRouteNotFound.Format(routeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result, ok := r.parse(args...); ok {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, v := range funcs {
|
||||||
|
// we don't want to override the user's LayoutFuncs, user should be able to override anything.
|
||||||
|
if s.config.Render.Template.HTMLTemplate.LayoutFuncs[k] == nil {
|
||||||
|
s.config.Render.Template.HTMLTemplate.LayoutFuncs[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.Render.Template.HTMLTemplate.Funcs[k] == nil {
|
||||||
|
s.config.Render.Template.HTMLTemplate.Funcs[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
s.templates = template.New(s.config.Render.Template)
|
s.templates = template.New(s.config.Render.Template)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -153,7 +184,6 @@ func (s *Iris) printBanner() {
|
||||||
|
|
||||||
c.Add(color.FgGreen)
|
c.Add(color.FgGreen)
|
||||||
stationsRunning++
|
stationsRunning++
|
||||||
|
|
||||||
c.Println()
|
c.Println()
|
||||||
if stationsRunning > 1 {
|
if stationsRunning > 1 {
|
||||||
c.Println("Server[" + strconv.Itoa(stationsRunning) + "]")
|
c.Println("Server[" + strconv.Itoa(stationsRunning) + "]")
|
||||||
|
@ -163,7 +193,7 @@ func (s *Iris) printBanner() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
printTicker.Start(time.Duration(2) * time.Millisecond)
|
printTicker.Start(time.Duration(1) * time.Millisecond)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,31 +72,24 @@ func Party(path string, handlersFn ...HandlerFunc) IParty {
|
||||||
|
|
||||||
// Handle registers a route to the server's router
|
// Handle registers a route to the server's router
|
||||||
// if empty method is passed then registers handler(s) for all methods, same as .Any
|
// if empty method is passed then registers handler(s) for all methods, same as .Any
|
||||||
func Handle(method string, registedPath string, handlers ...Handler) {
|
func Handle(method string, registedPath string, handlers ...Handler) IRoute {
|
||||||
DefaultIris.Handle(method, registedPath, handlers...)
|
return DefaultIris.Handle(method, registedPath, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleFunc registers a route with a method, path string, and a handler
|
// HandleFunc registers a route with a method, path string, and a handler
|
||||||
func HandleFunc(method string, path string, handlersFn ...HandlerFunc) {
|
func HandleFunc(method string, path string, handlersFn ...HandlerFunc) IRoute {
|
||||||
DefaultIris.HandleFunc(method, path, handlersFn...)
|
return DefaultIris.HandleFunc(method, path, handlersFn...)
|
||||||
}
|
|
||||||
|
|
||||||
// HandleAnnotated registers a route handler using a Struct implements iris.Handler (as anonymous property)
|
|
||||||
// which it's metadata has the form of
|
|
||||||
// `method:"path"` and returns the route and an error if any occurs
|
|
||||||
// handler is passed by func(urstruct MyStruct) Serve(ctx *Context) {}
|
|
||||||
//
|
|
||||||
// HandleAnnotated will be deprecated until the final v3 !
|
|
||||||
func HandleAnnotated(irisHandler Handler) error {
|
|
||||||
return DefaultIris.HandleAnnotated(irisHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API converts & registers a custom struct to the router
|
// API converts & registers a custom struct to the router
|
||||||
// receives three parameters
|
// receives two parameters
|
||||||
// first is the request path (string)
|
// first is the request path (string)
|
||||||
// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field
|
// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field.
|
||||||
// third are the common middlewares, is optional parameter
|
// third are the common middlewares, is optional parameter
|
||||||
//
|
//
|
||||||
|
// Note that API's routes have their default-name to the full registed path,
|
||||||
|
// no need to give a special name for it, because it's not supposed to be used inside your templates.
|
||||||
|
//
|
||||||
// Recommend to use when you retrieve data from an external database,
|
// Recommend to use when you retrieve data from an external database,
|
||||||
// and the router-performance is not the (only) thing which slows the server's overall performance.
|
// and the router-performance is not the (only) thing which slows the server's overall performance.
|
||||||
//
|
//
|
||||||
|
@ -173,53 +166,59 @@ func UseFunc(handlersFn ...HandlerFunc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get registers a route for the Get http method
|
// Get registers a route for the Get http method
|
||||||
func Get(path string, handlersFn ...HandlerFunc) {
|
func Get(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Get(path, handlersFn...)
|
return DefaultIris.Get(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post registers a route for the Post http method
|
// Post registers a route for the Post http method
|
||||||
func Post(path string, handlersFn ...HandlerFunc) {
|
func Post(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Post(path, handlersFn...)
|
return DefaultIris.Post(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put registers a route for the Put http method
|
// Put registers a route for the Put http method
|
||||||
func Put(path string, handlersFn ...HandlerFunc) {
|
func Put(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Put(path, handlersFn...)
|
return DefaultIris.Put(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete registers a route for the Delete http method
|
// Delete registers a route for the Delete http method
|
||||||
func Delete(path string, handlersFn ...HandlerFunc) {
|
func Delete(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Delete(path, handlersFn...)
|
return DefaultIris.Delete(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect registers a route for the Connect http method
|
// Connect registers a route for the Connect http method
|
||||||
func Connect(path string, handlersFn ...HandlerFunc) {
|
func Connect(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Connect(path, handlersFn...)
|
return DefaultIris.Connect(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head registers a route for the Head http method
|
// Head registers a route for the Head http method
|
||||||
func Head(path string, handlersFn ...HandlerFunc) {
|
func Head(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Head(path, handlersFn...)
|
return DefaultIris.Head(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options registers a route for the Options http method
|
// Options registers a route for the Options http method
|
||||||
func Options(path string, handlersFn ...HandlerFunc) {
|
func Options(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Options(path, handlersFn...)
|
return DefaultIris.Options(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch registers a route for the Patch http method
|
// Patch registers a route for the Patch http method
|
||||||
func Patch(path string, handlersFn ...HandlerFunc) {
|
func Patch(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Patch(path, handlersFn...)
|
return DefaultIris.Patch(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace registers a route for the Trace http methodd
|
// Trace registers a route for the Trace http methodd
|
||||||
func Trace(path string, handlersFn ...HandlerFunc) {
|
func Trace(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
DefaultIris.Trace(path, handlersFn...)
|
return DefaultIris.Trace(path, handlersFn...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete)
|
// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete)
|
||||||
func Any(path string, handlersFn ...HandlerFunc) {
|
func Any(path string, handlersFn ...HandlerFunc) []IRoute {
|
||||||
DefaultIris.Any(path, handlersFn...)
|
return DefaultIris.Any(path, handlersFn...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteByName returns a route by its name,if not found then returns a route with empty path
|
||||||
|
// Note that the searching is case-sensitive
|
||||||
|
func RouteByName(lookUpName string) IRoute {
|
||||||
|
return DefaultIris.RouteByName(lookUpName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticHandlerFunc returns a HandlerFunc to serve static system directory
|
// StaticHandlerFunc returns a HandlerFunc to serve static system directory
|
||||||
|
|
140
party.go
140
party.go
|
@ -19,20 +19,19 @@ import (
|
||||||
type (
|
type (
|
||||||
// IParty is the interface which implements the whole Party of routes
|
// IParty is the interface which implements the whole Party of routes
|
||||||
IParty interface {
|
IParty interface {
|
||||||
Handle(string, string, ...Handler)
|
Handle(string, string, ...Handler) IRoute
|
||||||
HandleFunc(string, string, ...HandlerFunc)
|
HandleFunc(string, string, ...HandlerFunc) IRoute
|
||||||
HandleAnnotated(Handler) error
|
|
||||||
API(path string, controller HandlerAPI, middlewares ...HandlerFunc) error
|
API(path string, controller HandlerAPI, middlewares ...HandlerFunc) error
|
||||||
Get(string, ...HandlerFunc)
|
Get(string, ...HandlerFunc) RouteNameFunc
|
||||||
Post(string, ...HandlerFunc)
|
Post(string, ...HandlerFunc) RouteNameFunc
|
||||||
Put(string, ...HandlerFunc)
|
Put(string, ...HandlerFunc) RouteNameFunc
|
||||||
Delete(string, ...HandlerFunc)
|
Delete(string, ...HandlerFunc) RouteNameFunc
|
||||||
Connect(string, ...HandlerFunc)
|
Connect(string, ...HandlerFunc) RouteNameFunc
|
||||||
Head(string, ...HandlerFunc)
|
Head(string, ...HandlerFunc) RouteNameFunc
|
||||||
Options(string, ...HandlerFunc)
|
Options(string, ...HandlerFunc) RouteNameFunc
|
||||||
Patch(string, ...HandlerFunc)
|
Patch(string, ...HandlerFunc) RouteNameFunc
|
||||||
Trace(string, ...HandlerFunc)
|
Trace(string, ...HandlerFunc) RouteNameFunc
|
||||||
Any(string, ...HandlerFunc)
|
Any(string, ...HandlerFunc) []IRoute
|
||||||
Use(...Handler)
|
Use(...Handler)
|
||||||
UseFunc(...HandlerFunc)
|
UseFunc(...HandlerFunc)
|
||||||
StaticHandlerFunc(systemPath string, stripSlashes int, compress bool, generateIndexPages bool, indexNames []string) HandlerFunc
|
StaticHandlerFunc(systemPath string, stripSlashes int, compress bool, generateIndexPages bool, indexNames []string) HandlerFunc
|
||||||
|
@ -61,13 +60,13 @@ func (p *GardenParty) IsRoot() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle registers a route to the server's router
|
// Handle registers a route to the server's router
|
||||||
// if empty method is passed then registers handler(s) for all methods, same as .Any
|
// if empty method is passed then registers handler(s) for all methods, same as .Any, but returns nil as result
|
||||||
func (p *GardenParty) Handle(method string, registedPath string, handlers ...Handler) {
|
func (p *GardenParty) Handle(method string, registedPath string, handlers ...Handler) IRoute {
|
||||||
if method == "" { // then use like it was .Any
|
if method == "" { // then use like it was .Any
|
||||||
for _, k := range AllMethods {
|
for _, k := range AllMethods {
|
||||||
p.Handle(k, registedPath, handlers...)
|
p.Handle(k, registedPath, handlers...)
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
path := fixPath(p.relativePath + registedPath) // keep the last "/" as default ex: "/xyz/"
|
path := fixPath(p.relativePath + registedPath) // keep the last "/" as default ex: "/xyz/"
|
||||||
if !p.station.config.DisablePathCorrection {
|
if !p.station.config.DisablePathCorrection {
|
||||||
|
@ -79,69 +78,14 @@ func (p *GardenParty) Handle(method string, registedPath string, handlers ...Han
|
||||||
p.station.plugins.DoPreHandle(route)
|
p.station.plugins.DoPreHandle(route)
|
||||||
p.station.addRoute(route)
|
p.station.addRoute(route)
|
||||||
p.station.plugins.DoPostHandle(route)
|
p.station.plugins.DoPostHandle(route)
|
||||||
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleFunc registers and returns a route with a method string, path string and a handler
|
// HandleFunc registers and returns a route with a method string, path string and a handler
|
||||||
// registedPath is the relative url path
|
// registedPath is the relative url path
|
||||||
// handler is the iris.Handler which you can pass anything you want via iris.ToHandlerFunc(func(res,req){})... or just use func(c *iris.Context)
|
// handler is the iris.Handler which you can pass anything you want via iris.ToHandlerFunc(func(res,req){})... or just use func(c *iris.Context)
|
||||||
func (p *GardenParty) HandleFunc(method string, registedPath string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) HandleFunc(method string, registedPath string, handlersFn ...HandlerFunc) IRoute {
|
||||||
p.Handle(method, registedPath, ConvertToHandlers(handlersFn)...)
|
return p.Handle(method, registedPath, ConvertToHandlers(handlersFn)...)
|
||||||
}
|
|
||||||
|
|
||||||
// HandleAnnotated registers a route handler using a Struct implements iris.Handler (as anonymous property)
|
|
||||||
// which it's metadata has the form of
|
|
||||||
// `method:"path"` and returns the route and an error if any occurs
|
|
||||||
// handler is passed by func(urstruct MyStruct) Serve(ctx *Context) {}
|
|
||||||
func (p *GardenParty) HandleAnnotated(irisHandler Handler) error {
|
|
||||||
var method string
|
|
||||||
var path string
|
|
||||||
var errMessage = ""
|
|
||||||
val := reflect.ValueOf(irisHandler).Elem()
|
|
||||||
|
|
||||||
for i := 0; i < val.NumField(); i++ {
|
|
||||||
typeField := val.Type().Field(i)
|
|
||||||
|
|
||||||
if typeField.Anonymous && typeField.Name == "Handler" {
|
|
||||||
tags := strings.Split(strings.TrimSpace(string(typeField.Tag)), " ")
|
|
||||||
firstTag := tags[0]
|
|
||||||
|
|
||||||
idx := strings.Index(string(firstTag), ":")
|
|
||||||
|
|
||||||
tagName := strings.ToUpper(string(firstTag[:idx]))
|
|
||||||
tagValue, unqerr := strconv.Unquote(string(firstTag[idx+1:]))
|
|
||||||
|
|
||||||
if unqerr != nil {
|
|
||||||
errMessage = errMessage + "\non getting path: " + unqerr.Error()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path = tagValue
|
|
||||||
avalaibleMethodsStr := strings.Join(AllMethods[0:], ",")
|
|
||||||
|
|
||||||
if !strings.Contains(avalaibleMethodsStr, tagName) {
|
|
||||||
//wrong method passed
|
|
||||||
errMessage = errMessage + "\nWrong method passed to the anonymous property iris.Handler -> " + tagName
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
method = tagName
|
|
||||||
|
|
||||||
} else {
|
|
||||||
errMessage = "\nStruct passed but it doesn't have an anonymous property of type iris.Hanndler, please refer to docs\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if errMessage == "" {
|
|
||||||
p.Handle(method, path, irisHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if errMessage != "" {
|
|
||||||
err = ErrHandleAnnotated.Format(errMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API converts & registers a custom struct to the router
|
// API converts & registers a custom struct to the router
|
||||||
|
@ -150,6 +94,9 @@ func (p *GardenParty) HandleAnnotated(irisHandler Handler) error {
|
||||||
// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field.
|
// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field.
|
||||||
// third are the common middlewares, is optional parameter
|
// third are the common middlewares, is optional parameter
|
||||||
//
|
//
|
||||||
|
// Note that API's routes have their default-name to the full registed path,
|
||||||
|
// no need to give a special name for it, because it's not supposed to be used inside your templates.
|
||||||
|
//
|
||||||
// Recommend to use when you retrieve data from an external database,
|
// Recommend to use when you retrieve data from an external database,
|
||||||
// and the router-performance is not the (only) thing which slows the server's overall performance.
|
// and the router-performance is not the (only) thing which slows the server's overall performance.
|
||||||
//
|
//
|
||||||
|
@ -300,56 +247,59 @@ func (p *GardenParty) API(path string, controller HandlerAPI, middlewares ...Han
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get registers a route for the Get http method
|
// Get registers a route for the Get http method
|
||||||
func (p *GardenParty) Get(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Get(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodGet, path, handlersFn...)
|
return p.HandleFunc(MethodGet, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post registers a route for the Post http method
|
// Post registers a route for the Post http method
|
||||||
func (p *GardenParty) Post(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Post(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodPost, path, handlersFn...)
|
return p.HandleFunc(MethodPost, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put registers a route for the Put http method
|
// Put registers a route for the Put http method
|
||||||
func (p *GardenParty) Put(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Put(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodPut, path, handlersFn...)
|
return p.HandleFunc(MethodPut, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete registers a route for the Delete http method
|
// Delete registers a route for the Delete http method
|
||||||
func (p *GardenParty) Delete(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Delete(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodDelete, path, handlersFn...)
|
return p.HandleFunc(MethodDelete, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect registers a route for the Connect http method
|
// Connect registers a route for the Connect http method
|
||||||
func (p *GardenParty) Connect(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Connect(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodConnect, path, handlersFn...)
|
return p.HandleFunc(MethodConnect, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head registers a route for the Head http method
|
// Head registers a route for the Head http method
|
||||||
func (p *GardenParty) Head(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Head(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodHead, path, handlersFn...)
|
return p.HandleFunc(MethodHead, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options registers a route for the Options http method
|
// Options registers a route for the Options http method
|
||||||
func (p *GardenParty) Options(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Options(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodOptions, path, handlersFn...)
|
return p.HandleFunc(MethodOptions, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch registers a route for the Patch http method
|
// Patch registers a route for the Patch http method
|
||||||
func (p *GardenParty) Patch(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Patch(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodPatch, path, handlersFn...)
|
return p.HandleFunc(MethodPatch, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace registers a route for the Trace http method
|
// Trace registers a route for the Trace http method
|
||||||
func (p *GardenParty) Trace(path string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Trace(path string, handlersFn ...HandlerFunc) RouteNameFunc {
|
||||||
p.HandleFunc(MethodTrace, path, handlersFn...)
|
return p.HandleFunc(MethodTrace, path, handlersFn...).Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete)
|
// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete)
|
||||||
func (p *GardenParty) Any(registedPath string, handlersFn ...HandlerFunc) {
|
func (p *GardenParty) Any(registedPath string, handlersFn ...HandlerFunc) []IRoute {
|
||||||
for _, k := range AllMethods {
|
theRoutes := make([]IRoute, len(AllMethods), len(AllMethods))
|
||||||
p.HandleFunc(k, registedPath, handlersFn...)
|
for idx, k := range AllMethods {
|
||||||
|
r := p.HandleFunc(k, registedPath, handlersFn...)
|
||||||
|
theRoutes[idx] = r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return theRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
// H_ is used to convert a context.IContext handler func to iris.HandlerFunc, is used only inside iris internal package to avoid import cycles
|
// H_ is used to convert a context.IContext handler func to iris.HandlerFunc, is used only inside iris internal package to avoid import cycles
|
||||||
|
|
|
@ -17,6 +17,9 @@ type (
|
||||||
Engine struct {
|
Engine struct {
|
||||||
Config *config.Template
|
Config *config.Template
|
||||||
Templates *template.Template
|
Templates *template.Template
|
||||||
|
// emptyFuncs returns empty functions, contains empty result for custom LayoutFuncs
|
||||||
|
|
||||||
|
emptyFuncs template.FuncMap
|
||||||
// Middleware
|
// Middleware
|
||||||
// Note:
|
// Note:
|
||||||
// I see that many template engines returns html/template as result
|
// I see that many template engines returns html/template as result
|
||||||
|
@ -26,27 +29,26 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var emptyFuncs = template.FuncMap{
|
|
||||||
"yield": func() (string, error) {
|
|
||||||
return "", fmt.Errorf("yield was called, yet no layout defined")
|
|
||||||
},
|
|
||||||
"partial": func() (string, error) {
|
|
||||||
return "", fmt.Errorf("block was called, yet no layout defined")
|
|
||||||
},
|
|
||||||
"current": func() (string, error) {
|
|
||||||
return "", nil
|
|
||||||
}, "render": func() (string, error) {
|
|
||||||
return "", nil
|
|
||||||
},
|
|
||||||
// just for test with jade
|
|
||||||
/*"bold": func() (string, error) {
|
|
||||||
return "", nil
|
|
||||||
},*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates and returns the HTMLTemplate template engine
|
// New creates and returns the HTMLTemplate template engine
|
||||||
func New(c config.Template) *Engine {
|
func New(c config.Template) *Engine {
|
||||||
return &Engine{Config: &c}
|
s := &Engine{Config: &c}
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"yield": func() (string, error) {
|
||||||
|
return "", fmt.Errorf("yield was called, yet no layout defined")
|
||||||
|
},
|
||||||
|
"partial": func() (string, error) {
|
||||||
|
return "", fmt.Errorf("block was called, yet no layout defined")
|
||||||
|
},
|
||||||
|
"current": func() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}, "render": func() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.emptyFuncs = funcs
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildTemplates builds the templates
|
// BuildTemplates builds the templates
|
||||||
|
@ -123,7 +125,7 @@ func (s *Engine) buildFromDir() error {
|
||||||
tmpl.Funcs(s.Config.HTMLTemplate.Funcs)
|
tmpl.Funcs(s.Config.HTMLTemplate.Funcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl.Funcs(emptyFuncs).Parse(contents)
|
tmpl.Funcs(s.emptyFuncs).Parse(contents)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,7 +172,7 @@ func (s *Engine) buildFromAsset() error {
|
||||||
tmpl.Funcs(s.Config.HTMLTemplate.Funcs)
|
tmpl.Funcs(s.Config.HTMLTemplate.Funcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl.Funcs(emptyFuncs).Parse(string(buf))
|
tmpl.Funcs(s.emptyFuncs).Parse(string(buf))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,12 +209,13 @@ func (s *Engine) layoutFuncsFor(name string, binding interface{}) {
|
||||||
buf, err := s.executeTemplateBuf(fullPartialName, binding)
|
buf, err := s.executeTemplateBuf(fullPartialName, binding)
|
||||||
// Return safe HTML here since we are rendering our own template.
|
// Return safe HTML here since we are rendering our own template.
|
||||||
return template.HTML(buf.String()), err
|
return template.HTML(buf.String()), err
|
||||||
|
|
||||||
},
|
},
|
||||||
// just for test with jade
|
}
|
||||||
/*"bold": func(content string) (template.HTML, error) {
|
_userLayoutFuncs := s.Config.HTMLTemplate.LayoutFuncs
|
||||||
return template.HTML("<b>" + content + "</b>"), nil
|
if _userLayoutFuncs != nil && len(_userLayoutFuncs) > 0 {
|
||||||
},*/
|
for k, v := range _userLayoutFuncs {
|
||||||
|
funcs[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if tpl := s.Templates.Lookup(name); tpl != nil {
|
if tpl := s.Templates.Lookup(name); tpl != nil {
|
||||||
tpl.Funcs(funcs)
|
tpl.Funcs(funcs)
|
||||||
|
|
115
route.go
115
route.go
|
@ -1,6 +1,8 @@
|
||||||
package iris
|
package iris
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,17 +13,33 @@ type (
|
||||||
GetMethod() string
|
GetMethod() string
|
||||||
GetDomain() string
|
GetDomain() string
|
||||||
GetPath() string
|
GetPath() string
|
||||||
|
GetName() string
|
||||||
|
// Name sets the name of the route
|
||||||
|
Name(string) IRoute
|
||||||
GetMiddleware() Middleware
|
GetMiddleware() Middleware
|
||||||
HasCors() bool
|
HasCors() bool
|
||||||
|
// used internaly to check arguments with the route's named parameters
|
||||||
|
parse(...interface{}) (string, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteNameFunc is returned to from route handle
|
||||||
|
RouteNameFunc func(string) IRoute
|
||||||
|
|
||||||
// Route contains basic and temporary info about the route in order to be stored to the tree
|
// Route contains basic and temporary info about the route in order to be stored to the tree
|
||||||
// It's struct because we pass it ( as IRoute) to the plugins
|
|
||||||
Route struct {
|
Route struct {
|
||||||
method string
|
method string
|
||||||
domain string
|
domain string
|
||||||
fullpath string
|
fullpath string
|
||||||
|
// the name of the route, the default name is just the registed path.
|
||||||
|
name string
|
||||||
middleware Middleware
|
middleware Middleware
|
||||||
|
|
||||||
|
// this is used to convert /mypath/:aparam/:something to -> /mypath/%v/%v and /mypath/* -> mypath/%v
|
||||||
|
// we use %v to escape from the conversions between strings,booleans and integers.
|
||||||
|
// used inside custom html template func 'url'
|
||||||
|
formattedPath string
|
||||||
|
// formattedParts is just the formattedPath count, used to see if we have one path parameter then the url's function arguments will be passed as one string to the %v
|
||||||
|
formattedParts int
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,11 +75,43 @@ func NewRoute(method string, registedPath string, middleware Middleware) *Route
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
r := &Route{method: method, domain: domain, fullpath: registedPath, middleware: middleware}
|
r := &Route{method: method, domain: domain, fullpath: registedPath, middleware: middleware, name: registedPath, formattedPath: registedPath}
|
||||||
|
r.formatPath()
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Route) formatPath() {
|
||||||
|
// we don't care about performance here, no runtime func.
|
||||||
|
|
||||||
|
n1Len := strings.Count(r.fullpath, ":")
|
||||||
|
isMatchEverything := r.fullpath[len(r.fullpath)-1] == MatchEverythingByte
|
||||||
|
if n1Len == 0 && !isMatchEverything {
|
||||||
|
// its a static
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n1Len == 0 && isMatchEverything {
|
||||||
|
//if we have something like: /mypath/anything/* -> /mypatch/anything/%v
|
||||||
|
r.formattedPath = r.fullpath[0:len(r.fullpath)-2] + "%v"
|
||||||
|
r.formattedParts++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tempPath := r.fullpath
|
||||||
|
|
||||||
|
splittedN1 := strings.Split(r.fullpath, "/")
|
||||||
|
|
||||||
|
for _, v := range splittedN1 {
|
||||||
|
if len(v) > 0 {
|
||||||
|
if v[0] == ':' || v[0] == MatchEverythingByte {
|
||||||
|
r.formattedParts++
|
||||||
|
tempPath = strings.Replace(tempPath, v, "%v", -1) // n1Len, but let it we don't care about performance here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
r.formattedPath = tempPath
|
||||||
|
}
|
||||||
|
|
||||||
// GetMethod returns the http method
|
// GetMethod returns the http method
|
||||||
func (r Route) GetMethod() string {
|
func (r Route) GetMethod() string {
|
||||||
return r.method
|
return r.method
|
||||||
|
@ -77,6 +127,17 @@ func (r Route) GetPath() string {
|
||||||
return r.fullpath
|
return r.fullpath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the route
|
||||||
|
func (r Route) GetName() string {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name sets the route's name
|
||||||
|
func (r *Route) Name(newName string) IRoute {
|
||||||
|
r.name = newName
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// GetMiddleware returns the chain of the []HandlerFunc registed to this Route
|
// GetMiddleware returns the chain of the []HandlerFunc registed to this Route
|
||||||
func (r Route) GetMiddleware() Middleware {
|
func (r Route) GetMiddleware() Middleware {
|
||||||
return r.middleware
|
return r.middleware
|
||||||
|
@ -87,6 +148,48 @@ func (r *Route) HasCors() bool {
|
||||||
return RouteConflicts(r, "httpmethod")
|
return RouteConflicts(r, "httpmethod")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used internaly to check arguments with the route's named parameters (iris.initTemplates for funcs)
|
||||||
|
func (r *Route) parse(args ...interface{}) (string, bool) {
|
||||||
|
// check if arguments are not equal to the named parameters ( : = 1, * = all named parameters split to / ), if this happens then send not found err
|
||||||
|
///TODO: I'm thinking of making an option to disable these checks and just return a result, because they have cost when rendering an html/template, not too big compared to the render action but... we will see
|
||||||
|
// can also do a check if this url can be realy served (_tree.rootBranch.GetBranch(path, ctx.Params)) and if not then return a 404 or a link to a ./templates/errors/404.html
|
||||||
|
// but we don't have access to the context itself(so we will have some memory allocations), although it's a good idea but let's keep things simple here.
|
||||||
|
argsLen := len(args)
|
||||||
|
// we have named parameters but arguments not given
|
||||||
|
if argsLen == 0 && r.formattedParts > 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have arguments but they are much more than the named parameters
|
||||||
|
|
||||||
|
// 1 check if we have /*, if yes then join all arguments to one as path and pass that as parameter
|
||||||
|
if argsLen > r.formattedParts {
|
||||||
|
if r.fullpath[len(r.fullpath)-1] == MatchEverythingByte {
|
||||||
|
// we have to convert each argument to a string in this case
|
||||||
|
|
||||||
|
argsString := make([]string, argsLen, argsLen)
|
||||||
|
|
||||||
|
for i, v := range args {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
argsString[i] = s
|
||||||
|
} else if num, ok := v.(int); ok {
|
||||||
|
argsString[i] = strconv.Itoa(num)
|
||||||
|
} else if b, ok := v.(bool); ok {
|
||||||
|
argsString[i] = strconv.FormatBool(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parameter := strings.Join(argsString, Slash)
|
||||||
|
result := fmt.Sprintf(r.formattedPath, parameter)
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
// 2 if !1 return false
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(r.formattedPath, args...), true
|
||||||
|
}
|
||||||
|
|
||||||
// RouteConflicts checks for route's middleware conflicts
|
// RouteConflicts checks for route's middleware conflicts
|
||||||
func RouteConflicts(r *Route, with string) bool {
|
func RouteConflicts(r *Route, with string) bool {
|
||||||
for _, h := range r.middleware {
|
for _, h := range r.middleware {
|
||||||
|
|
22
router.go
22
router.go
|
@ -59,7 +59,10 @@ type router struct {
|
||||||
garden *Garden
|
garden *Garden
|
||||||
methodMatch func(m1, m2 string) bool
|
methodMatch func(m1, m2 string) bool
|
||||||
getRequestPath func(*fasthttp.RequestCtx) []byte
|
getRequestPath func(*fasthttp.RequestCtx) []byte
|
||||||
ServeRequest func(reqCtx *fasthttp.RequestCtx)
|
// routes useful information, this info can be used to make custom links inside templates
|
||||||
|
// the route's information (can be) changed after its registration
|
||||||
|
lookups []IRoute
|
||||||
|
ServeRequest func(reqCtx *fasthttp.RequestCtx)
|
||||||
// errorPool is responsible to get the Context to handle not found errors
|
// errorPool is responsible to get the Context to handle not found errors
|
||||||
errorPool sync.Pool
|
errorPool sync.Pool
|
||||||
//it's true when optimize already ran
|
//it's true when optimize already ran
|
||||||
|
@ -89,6 +92,7 @@ func newRouter(station *Iris) *router {
|
||||||
garden: &Garden{},
|
garden: &Garden{},
|
||||||
methodMatch: methodMatchFunc,
|
methodMatch: methodMatchFunc,
|
||||||
getRequestPath: getRequestPathDefault,
|
getRequestPath: getRequestPathDefault,
|
||||||
|
lookups: make([]IRoute, 0),
|
||||||
HTTPErrorContainer: defaultHTTPErrors(),
|
HTTPErrorContainer: defaultHTTPErrors(),
|
||||||
GardenParty: &GardenParty{relativePath: "/", station: station, root: true},
|
GardenParty: &GardenParty{relativePath: "/", station: station, root: true},
|
||||||
errorPool: station.newContextPool()}
|
errorPool: station.newContextPool()}
|
||||||
|
@ -99,13 +103,27 @@ func newRouter(station *Iris) *router {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRoute calls the Plant, is created to set the router's station
|
// addRoute is a middleware between router and garden
|
||||||
|
// it just calls the garden's Plant method
|
||||||
|
// is 'thread-safe'
|
||||||
func (r *router) addRoute(route IRoute) {
|
func (r *router) addRoute(route IRoute) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
r.lookups = append(r.lookups, route)
|
||||||
r.garden.Plant(r.station, route)
|
r.garden.Plant(r.station, route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteByName returns a route by its name,if not found then returns a route with empty path
|
||||||
|
// Note that the searching is case-sensitive
|
||||||
|
func (r *router) RouteByName(lookUpName string) IRoute {
|
||||||
|
for _, route := range r.lookups {
|
||||||
|
if route.GetName() == lookUpName {
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Route{}
|
||||||
|
}
|
||||||
|
|
||||||
//check if any tree has cors setted to true, means that cors middleware is added
|
//check if any tree has cors setted to true, means that cors middleware is added
|
||||||
func (r *router) cors() (has bool) {
|
func (r *router) cors() (has bool) {
|
||||||
r.garden.visitAll(func(i int, tree *tree) {
|
r.garden.visitAll(func(i int, tree *tree) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user