Skip to content

Commit a3f944b

Browse files
committed
Add context.FindClosest(n) to find closest paths - useful for 404 pages to suggest valid pages
Former-commit-id: 90ff7c9da5369df5bd99fbbecf9955a8c555fea5
1 parent 4efe900 commit a3f944b

File tree

11 files changed

+137
-30
lines changed

11 files changed

+137
-30
lines changed

_examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Navigate through examples for a better understanding.
128128
- [Basic](routing/basic/main.go)
129129
- [Controllers](mvc)
130130
- [Custom HTTP Errors](routing/http-errors/main.go)
131+
- [Not Found - Suggest Closest Paths](routing/not-found-suggests/main.go) **NEW**
131132
- [Dynamic Path](routing/dynamic-path/main.go)
132133
* [root level wildcard path](routing/dynamic-path/root-wildcard/main.go)
133134
- [Write your own custom parameter types](routing/macros/main.go)

_examples/i18n/main.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func newApp() *iris.Application {
5454

5555
// Note that,
5656
// Iris automatically adds a "tr" global template function as well,
57-
// the only differene is the way you call it inside your templates and
57+
// the only difference is the way you call it inside your templates and
5858
// that it accepts a language code as its first argument: {{ tr "el-GR" "hi" "iris"}}
5959
})
6060
//
@@ -65,10 +65,18 @@ func newApp() *iris.Application {
6565
func main() {
6666
app := newApp()
6767

68-
// go to http://localhost:8080/el-gr/some-path (by path prefix)
69-
// or http://el.mydomain.com8080/some-path (by subdomain - test locally with the hosts file)
70-
// or http://localhost:8080/zh-CN/templates (by path prefix with uppercase)
71-
// or http://localhost:8080/some-path?lang=el-GR (by url parameter)
68+
// go to http://localhost:8080/el-gr/some-path
69+
// ^ (by path prefix)
70+
//
71+
// or http://el.mydomain.com8080/some-path
72+
// ^ (by subdomain - test locally with the hosts file)
73+
//
74+
// or http://localhost:8080/zh-CN/templates
75+
// ^ (by path prefix with uppercase)
76+
//
77+
// or http://localhost:8080/some-path?lang=el-GR
78+
// ^ (by url parameter)
79+
//
7280
// or http://localhost:8080 (default is en-US)
7381
// or http://localhost:8080/?lang=zh-CN
7482
//
@@ -77,6 +85,5 @@ func main() {
7785
// or http://localhost:8080/other?lang=en-US
7886
//
7987
// or use cookies to set the language.
80-
//
8188
app.Run(iris.Addr(":8080"), iris.WithSitemap("http://localhost:8080"))
8289
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import "github.com/kataras/iris/v12"
4+
5+
func main() {
6+
app := iris.New()
7+
app.OnErrorCode(iris.StatusNotFound, notFound)
8+
9+
// [register some routes...]
10+
app.Get("/home", handler)
11+
app.Get("/news", handler)
12+
app.Get("/news/politics", handler)
13+
app.Get("/user/profile", handler)
14+
app.Get("/user", handler)
15+
app.Get("/newspaper", handler)
16+
app.Get("/user/{id}", handler)
17+
18+
app.Run(iris.Addr(":8080"))
19+
}
20+
21+
func notFound(ctx iris.Context) {
22+
suggestPaths := ctx.FindClosest(3)
23+
if len(suggestPaths) == 0 {
24+
ctx.WriteString("404 not found")
25+
return
26+
}
27+
28+
ctx.HTML("Did you mean?<ul>")
29+
for _, s := range suggestPaths {
30+
ctx.HTML(`<li><a href="%s">%s</a></li>`, s, s)
31+
}
32+
ctx.HTML("</ul>")
33+
}
34+
35+
func handler(ctx iris.Context) {
36+
ctx.Writef("Path: %s", ctx.Path())
37+
}

configuration.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -388,19 +388,7 @@ func WithSitemap(startURL string) Configurator {
388388
}
389389

390390
for _, r := range app.GetRoutes() {
391-
if !r.IsOnline() {
392-
continue
393-
}
394-
395-
if r.Subdomain != "" {
396-
continue
397-
}
398-
399-
if r.Method != MethodGet {
400-
continue
401-
}
402-
403-
if len(r.Tmpl().Params) > 0 {
391+
if !r.IsStatic() || r.Subdomain != "" {
404392
continue
405393
}
406394

@@ -478,7 +466,7 @@ func WithSitemap(startURL string) Configurator {
478466
}
479467
}
480468
} else {
481-
app.HandleMany("GET HEAD", s.Path, handler)
469+
app.HandleMany("GET HEAD OPTIONS", s.Path, handler)
482470
}
483471

484472
}

context/application.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ type Application interface {
6060
// RouteExists reports whether a particular route exists
6161
// It will search from the current subdomain of context's host, if not inside the root domain.
6262
RouteExists(ctx Context, method, path string) bool
63+
// FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
64+
//
65+
// Order may change.
66+
FindClosestPaths(subdomain, searchPath string, n int) []string
6367
}

context/context.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ type Context interface {
310310
// Subdomain returns the subdomain of this request, if any.
311311
// Note that this is a fast method which does not cover all cases.
312312
Subdomain() (subdomain string)
313+
// FindClosest returns a list of "n" paths close to
314+
// this request based on subdomain and request path.
315+
//
316+
// Order may change.
317+
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/not-found-suggests
318+
FindClosest(n int) []string
313319
// IsWWW returns true if the current subdomain (if any) is www.
314320
IsWWW() bool
315321
// FullRqeuestURI returns the full URI,
@@ -1600,6 +1606,15 @@ func (ctx *context) Subdomain() (subdomain string) {
16001606
return
16011607
}
16021608

1609+
// FindClosest returns a list of "n" paths close to
1610+
// this request based on subdomain and request path.
1611+
//
1612+
// Order may change.
1613+
// Example: https://github.com/kataras/iris/tree/master/_examples/routing/not-found-suggests
1614+
func (ctx *context) FindClosest(n int) []string {
1615+
return ctx.Application().FindClosestPaths(ctx.Subdomain(), ctx.Path(), n)
1616+
}
1617+
16031618
// IsWWW returns true if the current subdomain (if any) is www.
16041619
func (ctx *context) IsWWW() bool {
16051620
host := ctx.Host()

context/route.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ type RouteReadOnly interface {
3131
// IsOnline returns true if the route is marked as "online" (state).
3232
IsOnline() bool
3333

34+
// IsStatic reports whether this route is a static route.
35+
// Does not contain dynamic path parameters,
36+
// is online and registered on GET HTTP Method.
37+
IsStatic() bool
3438
// StaticPath returns the static part of the original, registered route path.
3539
// if /user/{id} it will return /user
3640
// if /user/{id}/friend/{friendid:uint64} it will return /user too

core/router/handler.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ func NewDefaultHandler() RequestHandler {
7777
type RoutesProvider interface { // api builder
7878
GetRoutes() []*Route
7979
GetRoute(routeName string) *Route
80-
// GetStaticSites() []*StaticSite
81-
// Macros() *macro.Macros
8280
}
8381

8482
func (h *routerHandler) Build(provider RoutesProvider) error {

core/router/route.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func (r *Route) BuildHandlers() {
161161
}
162162

163163
// String returns the form of METHOD, SUBDOMAIN, TMPL PATH.
164-
func (r Route) String() string {
164+
func (r *Route) String() string {
165165
return fmt.Sprintf("%s %s%s",
166166
r.Method, r.Subdomain, r.Tmpl().Src)
167167
}
@@ -214,13 +214,13 @@ func (r *Route) SetPriority(prio float32) *Route {
214214
// Developer can get his registered path
215215
// via Tmpl().Src, Route.Path is the path
216216
// converted to match the underline router's specs.
217-
func (r Route) Tmpl() macro.Template {
217+
func (r *Route) Tmpl() macro.Template {
218218
return r.tmpl
219219
}
220220

221221
// RegisteredHandlersLen returns the end-developer's registered handlers, all except the macro evaluator handler
222222
// if was required by the build process.
223-
func (r Route) RegisteredHandlersLen() int {
223+
func (r *Route) RegisteredHandlersLen() int {
224224
n := len(r.Handlers)
225225
if handler.CanMakeHandler(r.tmpl) {
226226
n--
@@ -230,7 +230,7 @@ func (r Route) RegisteredHandlersLen() int {
230230
}
231231

232232
// IsOnline returns true if the route is marked as "online" (state).
233-
func (r Route) IsOnline() bool {
233+
func (r *Route) IsOnline() bool {
234234
return r.Method != MethodNone
235235
}
236236

@@ -270,11 +270,18 @@ func formatPath(path string) string {
270270
return path
271271
}
272272

273+
// IsStatic reports whether this route is a static route.
274+
// Does not contain dynamic path parameters,
275+
// is online and registered on GET HTTP Method.
276+
func (r *Route) IsStatic() bool {
277+
return r.IsOnline() && len(r.Tmpl().Params) == 0 && r.Method == "GET"
278+
}
279+
273280
// StaticPath returns the static part of the original, registered route path.
274281
// if /user/{id} it will return /user
275282
// if /user/{id}/friend/{friendid:uint64} it will return /user too
276283
// if /assets/{filepath:path} it will return /assets.
277-
func (r Route) StaticPath() string {
284+
func (r *Route) StaticPath() string {
278285
src := r.tmpl.Src
279286
bidx := strings.IndexByte(src, '{')
280287
if bidx == -1 || len(src) <= bidx {
@@ -289,7 +296,7 @@ func (r Route) StaticPath() string {
289296
}
290297

291298
// ResolvePath returns the formatted path's %v replaced with the args.
292-
func (r Route) ResolvePath(args ...string) string {
299+
func (r *Route) ResolvePath(args ...string) string {
293300
rpath, formattedPath := r.Path, r.FormattedPath
294301
if rpath == formattedPath {
295302
// static, no need to pass args
@@ -310,7 +317,7 @@ func (r Route) ResolvePath(args ...string) string {
310317

311318
// Trace returns some debug infos as a string sentence.
312319
// Should be called after Build.
313-
func (r Route) Trace() string {
320+
func (r *Route) Trace() string {
314321
printfmt := fmt.Sprintf("[%s:%d] %s:", r.SourceFileName, r.SourceLineNumber, r.Method)
315322
if r.Subdomain != "" {
316323
printfmt += fmt.Sprintf(" %s", r.Subdomain)

core/router/router.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"sync"
77

88
"github.com/kataras/iris/v12/context"
9+
10+
"github.com/schollz/closestmatch"
911
)
1012

1113
// Router is the "director".
@@ -23,10 +25,16 @@ type Router struct {
2325

2426
cPool *context.Pool // used on RefreshRouter
2527
routesProvider RoutesProvider
28+
29+
// key = subdomain
30+
// value = closest of static routes, filled on `BuildRouter/RefreshRouter`.
31+
closestPaths map[string]*closestmatch.ClosestMatch
2632
}
2733

2834
// NewRouter returns a new empty Router.
29-
func NewRouter() *Router { return &Router{} }
35+
func NewRouter() *Router {
36+
return &Router{}
37+
}
3038

3139
// RefreshRouter re-builds the router. Should be called when a route's state
3240
// changed (i.e Method changed at serve-time).
@@ -54,6 +62,28 @@ func (router *Router) AddRouteUnsafe(r *Route) error {
5462
return ErrNotRouteAdder
5563
}
5664

65+
// FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
66+
//
67+
// Order may change.
68+
func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []string {
69+
if router.closestPaths == nil {
70+
return nil
71+
}
72+
73+
cm, ok := router.closestPaths[subdomain]
74+
if !ok {
75+
return nil
76+
}
77+
78+
list := cm.ClosestN(searchPath, n)
79+
if len(list) == 1 && list[0] == "" {
80+
// yes, it may return empty string as its first slice element when not found.
81+
return nil
82+
}
83+
84+
return list
85+
}
86+
5787
// BuildRouter builds the router based on
5888
// the context factory (explicit pool in this case),
5989
// 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
110140
router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
111141
}
112142

143+
// build closest.
144+
subdomainPaths := make(map[string][]string)
145+
for _, r := range router.routesProvider.GetRoutes() {
146+
if !r.IsStatic() {
147+
continue
148+
}
149+
150+
subdomainPaths[r.Subdomain] = append(subdomainPaths[r.Subdomain], r.Path)
151+
}
152+
153+
router.closestPaths = make(map[string]*closestmatch.ClosestMatch)
154+
for subdomain, paths := range subdomainPaths {
155+
router.closestPaths[subdomain] = closestmatch.New(paths, []int{3, 4, 6})
156+
}
157+
113158
return nil
114159
}
115160

0 commit comments

Comments
 (0)