diff --git a/_examples/README.md b/_examples/README.md
index 7522d839..694e8b14 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -128,6 +128,7 @@ Navigate through examples for a better understanding.
- [Basic](routing/basic/main.go)
- [Controllers](mvc)
- [Custom HTTP Errors](routing/http-errors/main.go)
+- [Not Found - Suggest Closest Paths](routing/not-found-suggests/main.go) **NEW**
- [Dynamic Path](routing/dynamic-path/main.go)
* [root level wildcard path](routing/dynamic-path/root-wildcard/main.go)
- [Write your own custom parameter types](routing/macros/main.go)
diff --git a/_examples/i18n/main.go b/_examples/i18n/main.go
index fa2dc68c..d32806a8 100644
--- a/_examples/i18n/main.go
+++ b/_examples/i18n/main.go
@@ -54,7 +54,7 @@ func newApp() *iris.Application {
// Note that,
// Iris automatically adds a "tr" global template function as well,
- // the only differene is the way you call it inside your templates and
+ // the only difference is the way you call it inside your templates and
// that it accepts a language code as its first argument: {{ tr "el-GR" "hi" "iris"}}
})
//
@@ -65,10 +65,18 @@ func newApp() *iris.Application {
func main() {
app := newApp()
- // go to http://localhost:8080/el-gr/some-path (by path prefix)
- // or http://el.mydomain.com8080/some-path (by subdomain - test locally with the hosts file)
- // or http://localhost:8080/zh-CN/templates (by path prefix with uppercase)
- // or http://localhost:8080/some-path?lang=el-GR (by url parameter)
+ // go to http://localhost:8080/el-gr/some-path
+ // ^ (by path prefix)
+ //
+ // or http://el.mydomain.com8080/some-path
+ // ^ (by subdomain - test locally with the hosts file)
+ //
+ // or http://localhost:8080/zh-CN/templates
+ // ^ (by path prefix with uppercase)
+ //
+ // or http://localhost:8080/some-path?lang=el-GR
+ // ^ (by url parameter)
+ //
// or http://localhost:8080 (default is en-US)
// or http://localhost:8080/?lang=zh-CN
//
@@ -77,6 +85,5 @@ func main() {
// or http://localhost:8080/other?lang=en-US
//
// or use cookies to set the language.
- //
app.Run(iris.Addr(":8080"), iris.WithSitemap("http://localhost:8080"))
}
diff --git a/_examples/routing/not-found-suggests/main.go b/_examples/routing/not-found-suggests/main.go
new file mode 100644
index 00000000..e57002c9
--- /dev/null
+++ b/_examples/routing/not-found-suggests/main.go
@@ -0,0 +1,37 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ app := iris.New()
+ app.OnErrorCode(iris.StatusNotFound, notFound)
+
+ // [register some routes...]
+ app.Get("/home", handler)
+ app.Get("/news", handler)
+ app.Get("/news/politics", handler)
+ app.Get("/user/profile", handler)
+ app.Get("/user", handler)
+ app.Get("/newspaper", handler)
+ app.Get("/user/{id}", handler)
+
+ app.Run(iris.Addr(":8080"))
+}
+
+func notFound(ctx iris.Context) {
+ suggestPaths := ctx.FindClosest(3)
+ if len(suggestPaths) == 0 {
+ ctx.WriteString("404 not found")
+ return
+ }
+
+ ctx.HTML("Did you mean?
")
+ for _, s := range suggestPaths {
+ ctx.HTML(`- %s
`, s, s)
+ }
+ ctx.HTML("
")
+}
+
+func handler(ctx iris.Context) {
+ ctx.Writef("Path: %s", ctx.Path())
+}
diff --git a/configuration.go b/configuration.go
index 54d4a91c..b1c312d7 100644
--- a/configuration.go
+++ b/configuration.go
@@ -388,19 +388,7 @@ func WithSitemap(startURL string) Configurator {
}
for _, r := range app.GetRoutes() {
- if !r.IsOnline() {
- continue
- }
-
- if r.Subdomain != "" {
- continue
- }
-
- if r.Method != MethodGet {
- continue
- }
-
- if len(r.Tmpl().Params) > 0 {
+ if !r.IsStatic() || r.Subdomain != "" {
continue
}
@@ -478,7 +466,7 @@ func WithSitemap(startURL string) Configurator {
}
}
} else {
- app.HandleMany("GET HEAD", s.Path, handler)
+ app.HandleMany("GET HEAD OPTIONS", s.Path, handler)
}
}
diff --git a/context/application.go b/context/application.go
index 641d5fd0..9ad11227 100644
--- a/context/application.go
+++ b/context/application.go
@@ -60,4 +60,8 @@ type Application interface {
// RouteExists reports whether a particular route exists
// It will search from the current subdomain of context's host, if not inside the root domain.
RouteExists(ctx Context, method, path string) bool
+ // FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
+ //
+ // Order may change.
+ FindClosestPaths(subdomain, searchPath string, n int) []string
}
diff --git a/context/context.go b/context/context.go
index 168934ba..15c9a832 100644
--- a/context/context.go
+++ b/context/context.go
@@ -310,6 +310,12 @@ type Context interface {
// Subdomain returns the subdomain of this request, if any.
// Note that this is a fast method which does not cover all cases.
Subdomain() (subdomain string)
+ // FindClosest returns a list of "n" paths close to
+ // this request based on subdomain and request path.
+ //
+ // Order may change.
+ // Example: https://github.com/kataras/iris/tree/master/_examples/routing/not-found-suggests
+ FindClosest(n int) []string
// IsWWW returns true if the current subdomain (if any) is www.
IsWWW() bool
// FullRqeuestURI returns the full URI,
@@ -1600,6 +1606,15 @@ func (ctx *context) Subdomain() (subdomain string) {
return
}
+// FindClosest returns a list of "n" paths close to
+// this request based on subdomain and request path.
+//
+// Order may change.
+// Example: https://github.com/kataras/iris/tree/master/_examples/routing/not-found-suggests
+func (ctx *context) FindClosest(n int) []string {
+ return ctx.Application().FindClosestPaths(ctx.Subdomain(), ctx.Path(), n)
+}
+
// IsWWW returns true if the current subdomain (if any) is www.
func (ctx *context) IsWWW() bool {
host := ctx.Host()
diff --git a/context/route.go b/context/route.go
index f6776128..1927b9b1 100644
--- a/context/route.go
+++ b/context/route.go
@@ -31,6 +31,10 @@ type RouteReadOnly interface {
// IsOnline returns true if the route is marked as "online" (state).
IsOnline() bool
+ // IsStatic reports whether this route is a static route.
+ // Does not contain dynamic path parameters,
+ // is online and registered on GET HTTP Method.
+ IsStatic() bool
// StaticPath returns the static part of the original, registered route path.
// if /user/{id} it will return /user
// if /user/{id}/friend/{friendid:uint64} it will return /user too
diff --git a/core/router/handler.go b/core/router/handler.go
index 03fd3a15..8c0c992a 100644
--- a/core/router/handler.go
+++ b/core/router/handler.go
@@ -77,8 +77,6 @@ func NewDefaultHandler() RequestHandler {
type RoutesProvider interface { // api builder
GetRoutes() []*Route
GetRoute(routeName string) *Route
- // GetStaticSites() []*StaticSite
- // Macros() *macro.Macros
}
func (h *routerHandler) Build(provider RoutesProvider) error {
diff --git a/core/router/route.go b/core/router/route.go
index a1540b8e..d04f763a 100644
--- a/core/router/route.go
+++ b/core/router/route.go
@@ -161,7 +161,7 @@ func (r *Route) BuildHandlers() {
}
// String returns the form of METHOD, SUBDOMAIN, TMPL PATH.
-func (r Route) String() string {
+func (r *Route) String() string {
return fmt.Sprintf("%s %s%s",
r.Method, r.Subdomain, r.Tmpl().Src)
}
@@ -214,13 +214,13 @@ func (r *Route) SetPriority(prio float32) *Route {
// Developer can get his registered path
// via Tmpl().Src, Route.Path is the path
// converted to match the underline router's specs.
-func (r Route) Tmpl() macro.Template {
+func (r *Route) Tmpl() macro.Template {
return r.tmpl
}
// RegisteredHandlersLen returns the end-developer's registered handlers, all except the macro evaluator handler
// if was required by the build process.
-func (r Route) RegisteredHandlersLen() int {
+func (r *Route) RegisteredHandlersLen() int {
n := len(r.Handlers)
if handler.CanMakeHandler(r.tmpl) {
n--
@@ -230,7 +230,7 @@ func (r Route) RegisteredHandlersLen() int {
}
// IsOnline returns true if the route is marked as "online" (state).
-func (r Route) IsOnline() bool {
+func (r *Route) IsOnline() bool {
return r.Method != MethodNone
}
@@ -270,11 +270,18 @@ func formatPath(path string) string {
return path
}
+// IsStatic reports whether this route is a static route.
+// Does not contain dynamic path parameters,
+// is online and registered on GET HTTP Method.
+func (r *Route) IsStatic() bool {
+ return r.IsOnline() && len(r.Tmpl().Params) == 0 && r.Method == "GET"
+}
+
// StaticPath returns the static part of the original, registered route path.
// if /user/{id} it will return /user
// if /user/{id}/friend/{friendid:uint64} it will return /user too
// if /assets/{filepath:path} it will return /assets.
-func (r Route) StaticPath() string {
+func (r *Route) StaticPath() string {
src := r.tmpl.Src
bidx := strings.IndexByte(src, '{')
if bidx == -1 || len(src) <= bidx {
@@ -289,7 +296,7 @@ func (r Route) StaticPath() string {
}
// ResolvePath returns the formatted path's %v replaced with the args.
-func (r Route) ResolvePath(args ...string) string {
+func (r *Route) ResolvePath(args ...string) string {
rpath, formattedPath := r.Path, r.FormattedPath
if rpath == formattedPath {
// static, no need to pass args
@@ -310,7 +317,7 @@ func (r Route) ResolvePath(args ...string) string {
// Trace returns some debug infos as a string sentence.
// Should be called after Build.
-func (r Route) Trace() string {
+func (r *Route) Trace() string {
printfmt := fmt.Sprintf("[%s:%d] %s:", r.SourceFileName, r.SourceLineNumber, r.Method)
if r.Subdomain != "" {
printfmt += fmt.Sprintf(" %s", r.Subdomain)
diff --git a/core/router/router.go b/core/router/router.go
index b9649c9b..2cce0b01 100644
--- a/core/router/router.go
+++ b/core/router/router.go
@@ -6,6 +6,8 @@ import (
"sync"
"github.com/kataras/iris/v12/context"
+
+ "github.com/schollz/closestmatch"
)
// Router is the "director".
@@ -23,10 +25,16 @@ type Router struct {
cPool *context.Pool // used on RefreshRouter
routesProvider RoutesProvider
+
+ // key = subdomain
+ // value = closest of static routes, filled on `BuildRouter/RefreshRouter`.
+ closestPaths map[string]*closestmatch.ClosestMatch
}
// NewRouter returns a new empty Router.
-func NewRouter() *Router { return &Router{} }
+func NewRouter() *Router {
+ return &Router{}
+}
// RefreshRouter re-builds the router. Should be called when a route's state
// changed (i.e Method changed at serve-time).
@@ -54,6 +62,28 @@ func (router *Router) AddRouteUnsafe(r *Route) error {
return ErrNotRouteAdder
}
+// FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
+//
+// Order may change.
+func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []string {
+ if router.closestPaths == nil {
+ return nil
+ }
+
+ cm, ok := router.closestPaths[subdomain]
+ if !ok {
+ return nil
+ }
+
+ list := cm.ClosestN(searchPath, n)
+ if len(list) == 1 && list[0] == "" {
+ // yes, it may return empty string as its first slice element when not found.
+ return nil
+ }
+
+ return list
+}
+
// BuildRouter builds the router based on
// the context factory (explicit pool in this case),
// the request handler which manages how the main handler will multiplexes the routes
@@ -110,6 +140,21 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan
router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
}
+ // build closest.
+ subdomainPaths := make(map[string][]string)
+ for _, r := range router.routesProvider.GetRoutes() {
+ if !r.IsStatic() {
+ continue
+ }
+
+ subdomainPaths[r.Subdomain] = append(subdomainPaths[r.Subdomain], r.Path)
+ }
+
+ router.closestPaths = make(map[string]*closestmatch.ClosestMatch)
+ for subdomain, paths := range subdomainPaths {
+ router.closestPaths[subdomain] = closestmatch.New(paths, []int{3, 4, 6})
+ }
+
return nil
}
diff --git a/go.mod b/go.mod
index a0b1fb10..845eb1ac 100644
--- a/go.mod
+++ b/go.mod
@@ -27,6 +27,7 @@ require (
github.com/mediocregopher/radix/v3 v3.3.0
github.com/microcosm-cc/bluemonday v1.0.2
github.com/ryanuber/columnize v2.1.0+incompatible
+ github.com/schollz/closestmatch v2.1.0+incompatible
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
golang.org/x/text v0.3.0
gopkg.in/ini.v1 v1.51.0