Compare commits

...

10 Commits

11 changed files with 635 additions and 38 deletions

View File

@ -67,6 +67,8 @@ func CompileTo(target *Pattern, uncompiledPattern string) error {
target.init(defaultFieldTagName)
target.template = uncompiledPattern
s := uncompiledPattern
for {
index := strings.IndexRune(s, '{')

44
doc.go
View File

@ -1,12 +1,43 @@
/*
Package pathmatch provides pattern matching for paths.
Package pathmatch provides pattern matching for path templates.
For example, a path could be a file system path, or a path could be a path from a URL (such as an HTTP or HTTPS based URL).
A path template might look something like the following:
The matches can be loaded into variables (when using pathmatch.Find());
or can be loaded into a struct (when using pathmatch.Pattern.FindAndLoad()).
/v1/users/{user_id}
Example Usage:
Or:
/account={account_name}/user={user_name}/message={message_hash}
Or:
/backup/{folder_name}/
Or:
/v2/select/{fields}/from/{table_name}/where/{filters}
This path template could be a file system path, or a path could be a path from a URL (such as an HTTP or HTTPS based URL).
To compile one of these pattern templates, you would do something such as:
var template string = "/v1/users/{user_id}/messages/{message_id}"
var pattern pathmatch.Pattern
err := pathmatch.CompileTo(&pattern, template)
if nil != err {
fmt.Fprintf(os.Stdout, "ERROR: %s\n", err)
return
}
(In addition to the pathmatch.CompileTo() func, there is also the pathmatch.Compile(), and
pathmatch.MustCompile(). But pathmatch.CompileTo() is recommended over the other 2 options
for most cases.)
One you have the compiled pattern, you would either use pathmatch.Match(), pathmatch.Find(),
or pathmatch.FindAndLoad() depending on what you were trying to accomplish.
Example Usage
var pattern pathmatch.Pattern
@ -35,7 +66,7 @@ Example Usage:
fmt.Printf("user_id = %q \n", userId) // user_id = "bMM_kJFMEV"
fmt.Printf("vehicle_id = %q \n", vehicleId) // vehicle_id = "o_bcU.RZGK"
Alternate Example Usage:
Alternate Example Usage
var pattern pathmatch.Pattern
@ -64,6 +95,5 @@ Alternate Example Usage:
fmt.Printf("user_id = %q \n", data.UserId) // user_id = "bMM_kJFMEV"
fmt.Printf("vehicle_id = %q \n", data.VehicleId) // vehicle_id = "o_bcU.RZGK"
*/
package pathmatch

View File

@ -0,0 +1,26 @@
package pathmatch_test
import (
"github.com/reiver/go-pathmatch"
"fmt"
"os"
)
func ExamplePattern_String() {
var template = "/v1/users/{user_id}"
var pattern pathmatch.Pattern
err := pathmatch.CompileTo(&pattern, template)
if nil != err {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
return
}
fmt.Printf("pattern: %s", pattern)
// Output:
// pattern: /v1/users/{user_id}
}

View File

@ -32,6 +32,7 @@ import (
// }
type Pattern struct {
mutex sync.RWMutex
template string
bits []string
names []string
namesSet map[string]struct{}

View File

@ -12,6 +12,11 @@ var (
errThisShouldNeverHappen = newInternalError("This should never happen.")
)
// Find compares path against its (compiled) pattern template; if it matches it loads the
// matches into args, and then returns true.
//
// Find may set some, or all of the items in args even if it returns false, and even if it
// returns an error.
func (pattern *Pattern) Find(path string, args ...interface{}) (bool, error) {
if nil == pattern {
return false, errNilReceiver
@ -33,24 +38,33 @@ func (pattern *Pattern) Find(path string, args ...interface{}) (bool, error) {
s = s[len(bit):]
case wildcardBit:
index := strings.IndexRune(s, '/')
if -1 == index {
if err := set(s, argsIndex, args...); nil != err {
return doesNotMatter, err
}
argsIndex++
} else if 0 <= index {
value := s[:index]
if err := set(value, argsIndex, args...); nil != err {
return doesNotMatter, err
}
argsIndex++
s = s[index:]
} else {
return doesNotMatter, errThisShouldNeverHappen
if "" == s {
return false, nil
}
index := strings.IndexRune(s, '/')
var value string
switch {
default:
return doesNotMatter, errThisShouldNeverHappen
case -1 == index:
value = s
case 0 <= index:
value = s[:index]
}
if err := set(value, argsIndex, args...); nil != err {
return doesNotMatter, err
}
argsIndex++
s = s[len(value):]
}
}
if "" != s {
return false, nil
}
return true, nil
}

View File

@ -1,7 +1,9 @@
package pathmatch
package pathmatch_test
import (
"github.com/reiver/go-pathmatch"
"testing"
)
@ -9,13 +11,13 @@ import (
func TestFind(t *testing.T) {
tests := []struct{
Pattern *Pattern
Pattern *pathmatch.Pattern
Args []interface{}
Path string
ExpectedArgs []string
}{
{
Pattern: MustCompile("/{this}/{that}/{these}/{those}"),
Pattern: pathmatch.MustCompile("/{this}/{that}/{these}/{those}"),
Args: []interface{}{new(string), new(string), new(string), new(string), },
Path: "/apple/banana/cherry/grape",
ExpectedArgs: []string{"apple","banana","cherry","grape"},
@ -24,65 +26,65 @@ func TestFind(t *testing.T) {
{
Pattern: MustCompile("/user/{sessionKey}"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/DEFAULT"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/DEFAULT"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/DEFAULT/"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/DEFAULT/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "DEFAULT"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}/"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}/"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "DEFAULT"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/N9Z_tiv7",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "N9Z_tiv7"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}/"),
Pattern: pathmatch.MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}/"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/N9Z_tiv7/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "N9Z_tiv7"},

View File

@ -14,7 +14,13 @@ var (
)
func (pattern *Pattern) FindAndLoad(path string, strct interface{}) (bool, error) {
// FindAndLoad compares path against its (compiled) pattern template; if it matches
// it loads the matches into dest, and then returns true.
//
// dest can be a pointer struct, or a pointer to a []string.
//
// Find may set some, or all of the items or fields in dest even if it returns false, and even if it returns an error.
func (pattern *Pattern) FindAndLoad(path string, dest interface{}) (bool, error) {
if nil == pattern {
return false, errNilReceiver
}
@ -39,12 +45,59 @@ func (pattern *Pattern) FindAndLoad(path string, strct interface{}) (bool, error
return false, nil
}
reflectedValue := reflect.ValueOf(strct)
reflectedValue := reflect.ValueOf(dest)
if reflect.Ptr != reflectedValue.Kind() {
//@TODO: change error
return doesNotMatter, errExpectedAPointerToAStruct
}
reflectedValueElem := reflectedValue.Elem()
switch reflectedValueElem.Kind() {
case reflect.Slice:
var a []string = make([]string, len(args))
for i, arg := range args {
a[i] = *(arg.(*string))
}
return loadSlice(dest, a...)
case reflect.Struct:
return pattern.loadStruct(reflectedValueElem, args)
default:
//@TODO: change error
return doesNotMatter, errExpectedAPointerToAStruct
}
}
func loadSlice(dest interface{}, matches ...string) (bool, error) {
if nil == dest {
return false, errNilTarget
}
target, casted := dest.(*[]string)
if !casted {
//@TODO: CHANGE ERROR! ============================
return false, errExpectedAPointerToAStruct
}
if nil == target {
return false, errNilTarget
}
*target = (*target)[:0]
for _, match := range matches {
*target = append(*target, match)
}
return true, nil
}
func (pattern *Pattern) loadStruct(reflectedValueElem reflect.Value, args []interface{}) (bool, error) {
if nil == pattern {
return false, errNilReceiver
}
if reflect.Struct != reflectedValueElem.Kind() {
return doesNotMatter, errExpectedAPointerToAStruct
}
reflectedValueElemType := reflectedValueElem.Type()

View File

@ -4,11 +4,13 @@ package pathmatch
import (
"github.com/fatih/structs"
"reflect"
"testing"
)
func TestFindAndLoad(t *testing.T) {
func TestFindAndLoadStrucs(t *testing.T) {
tests := []struct{
Pattern *Pattern
@ -166,3 +168,73 @@ func TestFindAndLoad(t *testing.T) {
}
}
}
func TestFindAndLoadStrings(t *testing.T) {
tests := []struct{
Pattern *Pattern
Path string
Expected []string
}{
{
Pattern: MustCompile("/user/{sessionKey}"),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
Expected: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/{vehicleIdcode}/"),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT/",
Expected: []string{
"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
"DEFAULT",
},
},
{
Pattern: MustCompile("/{this}/{that}/{these}/{those}"),
Path: "/apple/banana/cherry/grape",
Expected: []string{
"apple",
"banana",
"cherry",
"grape",
},
},
}
for testNumber, test := range tests {
var actual []string
matched, err := test.Pattern.FindAndLoad(test.Path, &actual)
if nil != err {
t.Errorf("For test #%d, did not expect an error, but actually got one: (%T) %q", testNumber, err, err)
t.Logf("\tPATTERN: %q", test.Pattern)
t.Logf("\tPATH: %q", test.Path)
continue
}
if !matched {
t.Errorf("For test #%d, expected a match, but it didn't.", testNumber)
t.Logf("\tPATTERN: %q", test.Pattern)
t.Logf("\tPATH: %q", test.Path)
t.Log("\t--")
t.Logf("\tMATCHED: %t", matched)
continue
}
if expected := test.Expected; !reflect.DeepEqual(expected, actual) {
t.Errorf("For test #%d, did not get what was expected.", testNumber)
t.Logf("\tPATTERN: %q", test.Pattern)
t.Logf("\tPATH: %q", test.Path)
t.Log("\t--")
t.Logf("\tEXPECTED: %#v", expected)
t.Logf("\tACTUAL: %#v", actual)
continue
}
}
}

23
pattern_match.go 100644
View File

@ -0,0 +1,23 @@
package pathmatch
// Match returns true if path matches the compiled pattern, else returns false if it doesn't match.
func (receiver *Pattern) Match(path string) (bool, error) {
if nil == receiver {
return false, errNilReceiver
}
//@TODO: Is it a good idea to be dynamically creating this?
//@TODO: Also, can the struct fields be put in here directly instead?
args := []interface{}{}
numNames := len(receiver.MatchNames())
for i:=0; i<numNames; i++ {
args = append(args, new(string))
}
found, err := receiver.Find(path, args...)
if nil != err {
return false, err
}
return found, nil
}

View File

@ -0,0 +1,366 @@
package pathmatch_test
import (
"github.com/reiver/go-pathmatch"
"testing"
)
func TestPatternMatch(t *testing.T) {
tests := []struct{
Pattern string
Path string
Expected bool
}{
{
Pattern: "/v1/help",
Path: "/v1/help",
Expected: true,
},
{
Pattern: "/v1/help",
Path: "/v1/help/",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/help/me",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/help/me/",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/helping",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/helping/",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v2/help",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v2/HELP",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/apple",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/banana",
Expected: false,
},
{
Pattern: "/v1/help",
Path: "/v1/cherry",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/help/",
Expected: true,
},
{
Pattern: "/v1/help/",
Path: "/v1/help",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/help/me",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/help/me/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/helping",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/helping/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v2/help/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v2/HELP/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/apple/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/banana/",
Expected: false,
},
{
Pattern: "/v1/help/",
Path: "/v1/cherry/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1/user/123",
Expected: true,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1/user/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1/user/123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "//v1/user/123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1//user/123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1/user//123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "//v1//user/123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "/v1//user//123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "//v1//user//123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}",
Path: "//v1//user//123//",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1/user/123/",
Expected: true,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1/user/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1/user/123",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "//v1/user/123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1//user/123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1/user//123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "//v1//user/123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "/v1//user//123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "//v1//user//123/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/",
Path: "//v1//user//123//",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}",
Path: "/v1/user/123/contact/e-mail",
Expected: true,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}",
Path: "/v1/user/123/contact/e-mail/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}",
Path: "/v2/user/123/contact/e-mail",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}",
Path: "/v2/user/123/contact/e-mail",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}/",
Path: "/v1/user/123/contact/e-mail/",
Expected: true,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}/",
Path: "/v1/user/123/contact/e-mail",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}/",
Path: "/v2/user/123/contact/e-mail/",
Expected: false,
},
{
Pattern: "/v1/user/{user_id}/contact/{contact_type}/",
Path: "/v2/user/123/contact/e-mail/",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}",
Path: "/v1/company/acme",
Expected: true,
},
{
Pattern: "/v1/company/{company_name}",
Path: "/v1/company/acme/",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}",
Path: "/v2/company/acme",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}",
Path: "/v1/user/acme",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}",
Path: "/v1/COMPANY/acme",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}/",
Path: "/v1/company/acme/",
Expected: true,
},
{
Pattern: "/v1/company/{company_name}/",
Path: "/v1/company/acme",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}/",
Path: "/v2/company/acme/",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}/",
Path: "/v1/user/acme/",
Expected: false,
},
{
Pattern: "/v1/company/{company_name}/",
Path: "/v1/COMPANY/acme/",
Expected: false,
},
}
for testNumber, test := range tests {
var pattern pathmatch.Pattern
if err := pathmatch.CompileTo(&pattern, test.Pattern); nil != err {
t.Errorf("For test #%d, did not expect an error, but actually got one: (%T) %q", testNumber, err, err)
t.Errorf("\t: PATTERN: %q", test.Pattern)
t.Errorf("\t: PATH: %q", test.Path)
t.Errorf("\t: EXPECTED: %t", test.Expected)
continue
}
matched, err := pattern.Match(test.Path)
if nil != err {
t.Errorf("For test #%d, did not expect an error, but actually got one: (%T) %q", testNumber, err, err)
t.Errorf("\t: PATTERN: %q", test.Pattern)
t.Errorf("\t: PATH: %q", test.Path)
t.Errorf("\t: EXPECTED: %t", test.Expected)
continue
}
if expected, actual := test.Expected, matched; expected != actual {
t.Errorf("For test #%d, expected %t, but actually got %t.", testNumber, expected, actual)
t.Errorf("\t: PATTERN: %q", test.Pattern)
t.Errorf("\t: PATH: %q", test.Path)
t.Errorf("\t: EXPECTED: %t", test.Expected)
continue
}
}
}

View File

@ -0,0 +1,8 @@
package pathmatch
// String makes pathmatch.Pattern fit the fmt.Stringer interface.
//
// String returns the (pre-compiled) pattern template.
func (receiver Pattern) String() string {
return receiver.template
}