about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFranck Cuny <franck@lumberjaph.net>2013-04-25 20:01:23 -0700
committerFranck Cuny <franck@lumberjaph.net>2013-04-26 07:41:24 -0700
commitf1d0b1f2ce7a90b80079c8e0e2ef4642d36ec941 (patch)
tree85dd4aaa279abc49d0f6268c4bfc099aa18dc967
parentAdd build status (diff)
downloadpath-router-f1d0b1f2ce7a90b80079c8e0e2ef4642d36ec941.tar.gz
Add Validations for URL parameters.
By adding validations, it's possible to create without ambiguity routes
like "/user/:id" and "/user/:name", where the validation for the name
will check that it's an number, and name is a string.

A validation is not required for every key.  However, you can't specify
a validation for a key that does not exist in the URL. The validation
use Regexp for now, and the user has to create the regexp with the
function MustCompile.

Some utility functions are added to easily gain information about a
component, validator, etc.  This will be extended more soon.
-rw-r--r--request_test.go10
-rw-r--r--route.go77
-rw-r--r--router_test.go111
3 files changed, 178 insertions, 20 deletions
diff --git a/request_test.go b/request_test.go
index 715baae..c4ad3d5 100644
--- a/request_test.go
+++ b/request_test.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"net/url"
 	"testing"
+	"regexp"
 )
 
 func testRequestRoute(req *Request) (Response, error) {
@@ -13,7 +14,14 @@ func testRequestRoute(req *Request) (Response, error) {
 
 func buildDispatcher() Dispatcher {
 	router := BuildDispatcher()
-	router.AddRoute(&Route{Method: "GET", Path: "/foo/:bar", Code: testRequestRoute})
+	router.AddRoute(&Route{
+		Method: "GET",
+		Path:   "/foo/:bar",
+		Code:   testRequestRoute,
+		Validations: map[string]*regexp.Regexp{
+			"bar": regexp.MustCompile("\\w+"),
+		},
+	})
 	return router
 }
 
diff --git a/route.go b/route.go
index b724a04..9eab0ab 100644
--- a/route.go
+++ b/route.go
@@ -17,6 +17,7 @@ type Route struct {
 	optionalNamedComponents map[string]bool
 	length                  int
 	lengthWithoutOptional   int
+	Validations             map[string]*regexp.Regexp
 }
 
 type Match struct {
@@ -31,11 +32,19 @@ var componentsIsOptional = regexp.MustCompile("^\\?:")
 var namedComponentsRegex = regexp.MustCompile("^:(.*)$")
 var convertComponent = regexp.MustCompile("^\\??:(.*)$")
 
+// XXX explain this function
 func (self *Route) convertComponentName(name string) string {
 	newName := convertComponent.FindStringSubmatch(name)
+	if len(newName) == 0 {
+		return name
+	}
 	return newName[1]
 }
 
+func (self *Route) ValidateFor(key string, component string) bool {
+	return self.Validations[key].MatchString(component)
+}
+
 func (self *Route) Match(method string, components []string) *Match {
 
 	if self.Method != method {
@@ -48,15 +57,35 @@ func (self *Route) Match(method string, components []string) *Match {
 
 	mapping := map[string]string{}
 
-	for i, c := range self.components {
-		if componentsIsOptional.MatchString(c) {
-			break
-		}
-		p := components[i]
+	currentComponentsLength := len(components)
 
-		if componentIsVariable.MatchString(c) == true {
-			mapping[self.convertComponentName(c)] = p
-		} else {
+L:
+	for i, c := range self.components {
+		convertedName := self.convertComponentName(c)
+
+		switch {
+		case self.IsOptionalUrlParameter(convertedName) == true:
+			if i < currentComponentsLength && self.HasValidationFor(convertedName) {
+				p := components[i]
+				if self.ValidateFor(convertedName, p) == true {
+					break L
+				}
+			} else {
+				break L
+			}
+		case self.IsRequiredUrlParameter(convertedName) == true:
+			p := components[i]
+			if self.HasValidationFor(convertedName) {
+				if self.ValidateFor(convertedName, p) == true {
+					mapping[convertedName] = p
+				} else {
+					return nil
+				}
+			} else {
+				mapping[convertedName] = p
+			}
+		default:
+			p := components[i]
 			if p != c {
 				return nil
 			}
@@ -85,6 +114,34 @@ func (self *Route) init() {
 	self.length = len(self.components)
 
 	self.getNamedComponents()
+
+	for k, _ := range self.Validations {
+		if self.IsUrlParameter(k) == false {
+			panic(k + " is missing")
+		}
+	}
+}
+
+func (self *Route) IsUrlParameter(component string) bool {
+	if self.IsRequiredUrlParameter(component) == false && self.IsOptionalUrlParameter(component) == false {
+		return false
+	}
+	return true
+}
+
+func (self *Route) IsRequiredUrlParameter(component string) bool {
+	return self.requiredNamedComponents[component]
+}
+
+func (self *Route) IsOptionalUrlParameter(component string) bool {
+	return self.optionalNamedComponents[component]
+}
+
+func (self *Route) HasValidationFor(component string) bool {
+	if self.Validations[component] != nil {
+		return true
+	}
+	return false
 }
 
 func (self *Route) getNamedComponents() {
@@ -93,9 +150,9 @@ func (self *Route) getNamedComponents() {
 
 	for _, c := range self.components {
 		if namedComponentsRegex.MatchString(c) == true {
-			self.requiredNamedComponents[c] = true
+			self.requiredNamedComponents[self.convertComponentName(c)] = true
 		} else if componentsIsOptional.MatchString(c) == true {
-			self.optionalNamedComponents[c] = true
+			self.optionalNamedComponents[self.convertComponentName(c)] = true
 		}
 	}
 	self.lengthWithoutOptional = self.length - len(self.optionalNamedComponents)
diff --git a/router_test.go b/router_test.go
index 8ee2bcc..1a03764 100644
--- a/router_test.go
+++ b/router_test.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"regexp"
 	"testing"
 )
 
@@ -15,7 +16,16 @@ func testRoute(req *Request) (Response, error) {
 func TestBasic(t *testing.T) {
 	router := BuildDispatcher()
 	router.AddRoute(&Route{Method: "GET", Path: "/", Code: testRoute})
-	router.AddRoute(&Route{Method: "GET", Path: "/blog/:year/:month/:day", Code: testRoute})
+	router.AddRoute(&Route{
+		Method: "GET",
+		Path:   "/blog/:year/:month/:day",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"year":  regexp.MustCompile("[\\d]{4}"),
+			"month": regexp.MustCompile("[\\d]{2}"),
+			"day":   regexp.MustCompile("[\\d]{2}"),
+		},
+	})
 	router.AddRoute(&Route{Method: "GET", Path: "/blog", Code: testRoute})
 
 	if router.routes[0].Path != "/" {
@@ -29,16 +39,63 @@ func TestBasic(t *testing.T) {
 	}
 }
 
+func TestWithSimleValidation(t *testing.T) {
+	router := BuildDispatcher()
+	router.AddRoute(&Route{
+		Path:   "/users/:id",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"id": regexp.MustCompile("[\\d]{1}"),
+		},
+	})
+	router.AddRoute(&Route{
+		Path:   "/users/:name",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"name": regexp.MustCompile("[a-zA-Z]+"),
+		},
+	})
+
+	r := &http.Request{Method: "GET", URL: &url.URL{Path: "/users/1"}}
+	m, _ := router.Match(r)
+	if m == nil {
+		t.Fatal()
+	}
+	if m.Route.Path != "/users/:id" {
+		t.Fatal()
+	}
+
+	r = &http.Request{Method: "GET", URL: &url.URL{Path: "/users/foo"}}
+	m, _ = router.Match(r)
+	if m == nil {
+		t.Fatal()
+	}
+	if m.Route.Path != "/users/:name" {
+		t.Fatal()
+	}
+}
+
 func TestMatch(t *testing.T) {
 	router := BuildDispatcher()
-	router.AddRoute(&Route{Method: "GET", Path: "/blog/:year/:month/:day", Code: testRoute})
+	router.AddRoute(&Route{
+		Method: "GET",
+		Path:   "/blog/:year/:month/:day",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"year":  regexp.MustCompile("[\\d]{4}"),
+			"month": regexp.MustCompile("[\\d]{2}"),
+			"day":   regexp.MustCompile("[\\d]{2}"),
+		},
+	})
 	router.AddRoute(&Route{Method: "GET", Path: "/blog", Code: testRoute})
 
 	pathToTests := []url.URL{
 		url.URL{Path: "/blog"},
 		url.URL{Path: "/blog"},
-		url.URL{Path: "/blog/2013/4/21"},
-		url.URL{Path: "/blog/2013/21/4"},
+		url.URL{Path: "/blog/2013/04/21"},
+		url.URL{Path: "/blog/2013/21/04"},
 	}
 	for _, p := range pathToTests {
 		r := &http.Request{URL: &p, Method: "GET"}
@@ -53,8 +110,23 @@ func TestMatch(t *testing.T) {
 
 func TestMatchOptional(t *testing.T) {
 	router := BuildDispatcher()
-	router.AddRoute(&Route{Path: "/blog/?:year", Method: "GET", Code: testRoute})
-	router.AddRoute(&Route{Path: "/blog/:year/?:month", Method: "GET", Code: testRoute})
+	router.AddRoute(&Route{
+		Path:   "/blog/?:year",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"year": regexp.MustCompile("[\\d]{4}"),
+		},
+	})
+	router.AddRoute(&Route{
+		Path:   "/blog/:year/?:month",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"year":  regexp.MustCompile("[\\d]{4}"),
+			"month": regexp.MustCompile("[\\d]{2}"),
+		},
+	})
 
 	pathToTests := []url.URL{
 		url.URL{Path: "/blog"},
@@ -76,7 +148,14 @@ func TestMatchOptional(t *testing.T) {
 func TestAmbigiousSimple(t *testing.T) {
 	router := BuildDispatcher()
 	router.AddRoute(&Route{Path: "/foo/bar", Method: "GET", Code: testRoute})
-	router.AddRoute(&Route{Path: "/foo/:bar", Method: "GET", Code: testRoute})
+	router.AddRoute(&Route{
+		Path:   "/foo/:bar",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"bar": regexp.MustCompile("[a-zA-Z]{3}"),
+		},
+	})
 
 	r := &http.Request{Method: "GET", URL: &url.URL{Path: "/foo/bar"}}
 	m, _ := router.Match(r)
@@ -89,8 +168,22 @@ func TestAmbigiousSimple(t *testing.T) {
 
 func TestAmbigiousFail(t *testing.T) {
 	router := BuildDispatcher()
-	router.AddRoute(&Route{Path: "/foo/:bar", Method: "GET", Code: testRoute})
-	router.AddRoute(&Route{Path: "/:foo/bar", Method: "GET", Code: testRoute})
+	router.AddRoute(&Route{
+		Path:   "/foo/:bar",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"bar": regexp.MustCompile("[a-zA-Z]{3}"),
+		},
+	})
+	router.AddRoute(&Route{
+		Path:   "/:foo/bar",
+		Method: "GET",
+		Code:   testRoute,
+		Validations: map[string]*regexp.Regexp{
+			"foo": regexp.MustCompile("[a-zA-Z]{3}"),
+		},
+	})
 
 	r := &http.Request{Method: "GET", URL: &url.URL{Path: "/foo/bar"}}
 	m, err := router.Match(r)