initial commit. still need to create more tests.

master
Charles Iliya Krempeaux 2016-02-25 14:59:38 -08:00
commit 05476fffbb
19 changed files with 2321 additions and 0 deletions

19
LICENSE 100644
View File

@ -0,0 +1,19 @@
Copyright (c) 2016 Charles Iliya Krempeaux <charles@reptile.ca> :: http://changelog.ca/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

79
README.md 100644
View File

@ -0,0 +1,79 @@
# go-pathmatch
A library that provides *pattern matching* for paths, for the Go programming language.
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).
## Documention
Online documentation, which includes examples, can be found at: http://godoc.org/github.com/reiver/go-pathmatch
[![GoDoc](https://godoc.org/github.com/reiver/go-pathmatch?status.svg)](https://godoc.org/github.com/reiver/go-pathmatch)
## Example Usage
```
import (
"github.com/reiver/go-pathmatch"
)
// ...
pattern, err := pathmatch.Compile("/users/{user_id}/vehicles/{vehicle_id}}")
if nil != err {
//@TODO
}
var userId string
var vehicleId string
didMatch, err := pattern.Match("/users/bMM_kJFMEV/vehicles/o_bcU.RZGK", &userId, &vehicleId)
if nil != err {
//@TODO
}
if didMatch {
fmt.Println("The path matched!")
fmt.Printf("user_id = %q \n", userId) // user_id = "bMM_kJFMEV"
fmt.Printf("vehicle_id = %q \n", vehicleId) // vehicle_id = "o_bcU.RZGK"
} else {
fmt.Println("The patch did not match.")
}
```
Alternatively:
```
import (
"github.com/reiver/go-pathmatch"
)
// ...
pattern, err := pathmatch.Compile("/users/{user_id}/vehicles/{vehicle_id}}")
if nil != err {
//@TODO
}
data := struct{
UserId string `match:"user_id"`
VehicleId string `match:"vehicle_id"`
}{}
didMatch, err := pattern.MatchAndLoad("/users/bMM_kJFMEV/vehicles/o_bcU.RZGK", &data)
if nil != err {
//@TODO
}
if didMatch {
fmt.Println("The path matched!")
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"
} else {
fmt.Println("The patch did not match.")
}
```

View File

@ -0,0 +1,121 @@
package pathmatch
// BadRequestComplainer is used to represent one of the types of errors that could be returned when
// calling the pathmatch.Compile func, the pathmatch.Pattern.Match method, or the pathmatch.Pattern.MatchAndLoad
// method. The meaning of this type of error is that the problem was due to something whomever called the func or method did.
//
// For example, maybe the uncompiled pattern passed to the pathmatch.Compile() func had
// a syntax error in it. Or, also for example, maybe the type of parameter passed to
// pathmatch.Pattern.Match() was of the wrong type. Etc.
//
// Example usage of BadRequestComplainer with pathmatch.Compile():
//
// pattern, err := pathmatch.Compile("/fruits/{there_is_an_error_in_here/") // ← The uncompiled pattern there has an error in it.
// if nil != err {
// switch err.(type) { // ← Note that we are using a Go type-switch here.
//
// case pathmatch.BadRequestComplainer: // ← Here we are detecting if the error returned is a pathmatch.BadRequestComplainer.
//
// fmt.Printf("Something you did when you called pathmatch.Compile() caused an error. The error message was....\n%s\n", err.Error())
// return
//
// case pathmatch.InternalErrorComplainer:
//
// fmt.Printf("It's not your fault; it's our fault. Something bad happened internally when pathmatch.Compile() was running. The error message was....\n%s\n", err.Error())
// return
//
// default:
//
// fmt.Printf("Some kind of unexpected error happend: %v", err)
// return
// }
// }
//
// Somewhat continuing this example (although without the error in the uncompiled pattern), we might then
// use the pathmatch.Pattern.Match() method, which could also generate an error that fits the BadRequestComplainer
// interface.
//
// Example usage of BadRequestComplainer with pathmatch.Pattern.Match():
//
// pattern, err := pathmatch.Compile("/users/{user_id}/cards/{fruit_id}")
// if nil != err {
// switch err.(type) {
//
// case pathmatch.BadRequestComplainer:
//
// fmt.Printf("Something you did when you called pathmatch.Compile() caused an error. The error message was....\n%s\n", err.Error())
// return
//
// case pathmatch.InternalErrorComplainer:
//
// fmt.Printf("It's not your fault; it's our fault. Something bad happened internally when pathmatch.Compile() was running. The error message was....\n%s\n", err.Error())
// return
//
// default:
//
// fmt.Printf("Some kind of unexpected error happend: %v", err)
// return
// }
// }
//
// var userId string
// var cardId string
//
// didMatch, err := pattern.Match("/users/8sN.oP/cards/X3j_T4", userId, cardId)
// if nil != err {
// switch err.(type) { // ← Note that we are using a Go type-switch here.
//
// case pathmatch.BadRequestComplainer: // ← Here we are detecting if the error returned is a pathmatch.BadRequestComplainer.
//
// fmt.Printf("Something you did when you called pattern.Match() caused an error. The error message was....\n%s\n", err.Error())
// return
//
// case pathmatch.InternalErrorComplainer:
//
// fmt.Printf("It's not your fault; it's our fault. Something bad happened internally when pattern.Match() was running. The error message was....\n%s\n", err.Error())
// return
//
// default:
//
// fmt.Printf("Some kind of unexpected error happend: %v", err)
// return
// }
// }
//
// Note that one can get more specific than just a BadRequestComplainer. For example:
// NotEnoughArgumentsComplainer, PatternSyntaxErrorComplainer, UnsupportedArgumentTypeComplainer,
// and StructFieldWrongTypeComplainer.
//
// To be able to detect those more specific error types, put them BEFORE the "case pathmatch.BadRequestComplainer:"
// in the type switch. For example:
//
// pattern, err := pathmatch.Compile("/users/{user_id}/cards/{fruit_id}")
// if nil != err {
// switch err.(type) {
//
// case pathmatch.PatternSyntaxErrorComplainer: // ← Here we are detecting if the error returned was due to a syntax error, in the uncompiled pattern. Also note that it comes BEFORE the 'pathmatch.BadRequestComplainer' case; THAT IS IMPORTANT!
//
// fmt.Printf("The uncompiled pattern passed to pathmatch.Compile() had a syntax error in it. The error message describing the syntax error is....\n%s\n", err.Error())
// return
//
// case pathmatch.BadRequestComplainer:
//
// fmt.Printf("Something you did when you called pathmatch.Compile() caused an error. The error message was....\n%s\n", err.Error())
// return
//
// case pathmatch.InternalErrorComplainer:
//
// fmt.Printf("It's not your fault; it's our fault. Something bad happened internally when pathmatch.Compile() was running. The error message was....\n%s\n", err.Error())
// return
//
// default:
//
// fmt.Printf("Some kind of unexpected error happend: %v", err)
// return
// }
// }
type BadRequestComplainer interface {
error
BadRequestComplainer()
}

114
compile.go 100644
View File

@ -0,0 +1,114 @@
package pathmatch
import (
"strings"
)
const (
defaultFieldTagName = "match"
wildcardBit = "{}"
)
var (
errMissingEndingRightBraceToMatchBeginningLeftBrace = newPatternSyntaxErrorComplainer(`Missing ending "}" (to match beginning "{").`)
errSlashInsideOfBraces = newPatternSyntaxErrorComplainer(`"/" inside of "{...}".`)
errLeftBraceInsideOfBraces = newPatternSyntaxErrorComplainer(`"{" inside of "{...}".`)
)
// Compile takes an uncompiled pattern, in the form of a Go string (ex: "/users/{userId}/vehicles/{vehicleId}"),
// and returns a compiled pattern.
//
// The compiled pattern can the be used to test if a path matches the pattern it contains.
//
// If the uncompiled pattern has a syntax error, Compile returns an error.
//
// Example Usage:
//
// pattern, err := pathmath.Compile("/users/{user_id}")
// if nil != err {
// fmt.Printf("ERROR Compiling: %v\n", err)
// return
// }
func Compile(uncompiledPattern string) (Pattern, error) {
pattern := newPattern(defaultFieldTagName)
s := uncompiledPattern
for {
index := strings.IndexRune(s, '{')
if -1 == index {
pattern.bits = append(pattern.bits, s)
break
}
bit := s[:index]
if "" != bit { // This is to deal with the case where a {???} is right at the beginning of the uncompiledPattern.
pattern.bits = append(pattern.bits, bit)
}
s = s[1+index:]
if "" == s {
break
}
index = strings.IndexRune(s, '}')
if -1 == index {
return nil, errMissingEndingRightBraceToMatchBeginningLeftBrace
}
// There should not be a slash ("/") before the ending brace ("}").
// If there is, it is a syntax error.
slashIndex := strings.IndexRune(s, '/')
if -1 != slashIndex && slashIndex <= index {
return nil, errSlashInsideOfBraces
}
// There should not be another beginning brace ("{") before the ending brace ("}").
// If there is, it is a syntax error.
anotherLeftBraceIndex := strings.IndexRune(s, '{')
if -1 != anotherLeftBraceIndex && anotherLeftBraceIndex <= index {
return nil, errLeftBraceInsideOfBraces
}
bit = s[:index]
// Match names should be unique, within a pattern.
if _, ok := pattern.namesSet[bit]; ok {
return nil, newPatternSyntaxErrorComplainer("Duplicate match name: %q.", bit)
}
pattern.names = append(pattern.names, bit)
pattern.namesSet[bit] = struct{}{}
pattern.bits = append(pattern.bits, wildcardBit)
s = s[1+index:]
if "" == s {
break
}
}
return pattern, nil
}
// MustCompile is like Compile except that it never returns an error; but
// instead panic()s if there was an error.
//
// Example Usage:
//
// pattern := pathmath.MustCompile("/users/{user_id}")
//
// Note that if one recover()s from the panic(), one can use a Go type-switch
// to figure out what kind of error it is.
func MustCompile(uncompiledPattern string) Pattern {
if pattern, err := Compile(uncompiledPattern); nil != err {
panic(err)
} else {
return pattern
}
}

560
compile_test.go 100644
View File

@ -0,0 +1,560 @@
package pathmatch
import (
"testing"
)
func TestCompileAndMatchNames(t *testing.T) {
tests := []struct{
UncompiledPattern string
ExpectedNames []string
ExpectedNamesSet map[string]struct{}
ExpectedBits []string
}{
{
UncompiledPattern: "",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"",
},
},
{
UncompiledPattern: "/",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"/",
},
},
{
UncompiledPattern: "//",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"//",
},
},
{
UncompiledPattern: "///",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"///",
},
},
{
UncompiledPattern: "/{this}",
ExpectedNames:[]string{"this"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
},
},
{
UncompiledPattern: "/{this}/",
ExpectedNames:[]string{"this"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "/{this}/{that}",
ExpectedNames:[]string{"this","that"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "/{this}/{that}/",
ExpectedNames:[]string{"this","that"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "/{this}/{that}/{these}",
ExpectedNames:[]string{"this","that","these"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "/{this}/{that}/{these}/",
ExpectedNames:[]string{"this","that","these"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "/{this}/{that}/{these}/{those}",
ExpectedNames:[]string{"this","that","these","those"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
"those":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "/{this}/{that}/{these}/{those}/",
ExpectedNames:[]string{"this","that","these","those"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
"those":struct{}{},
},
ExpectedBits: []string{
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "{this}",
ExpectedNames:[]string{"this"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
},
},
{
UncompiledPattern: "{this}/",
ExpectedNames:[]string{"this"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
},
},
{
UncompiledPattern: "{this}/{that}",
ExpectedNames:[]string{"this","that"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "{this}/{that}/",
ExpectedNames:[]string{"this","that"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "{this}/{that}/{these}",
ExpectedNames:[]string{"this","that","these"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "{this}/{that}/{these}/",
ExpectedNames:[]string{"this","that","these"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "{this}/{that}/{these}/{those}",
ExpectedNames:[]string{"this","that","these","those"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
"those":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
},
},
{
UncompiledPattern: "{this}/{that}/{these}/{those}/",
ExpectedNames:[]string{"this","that","these","those"},
ExpectedNamesSet:map[string]struct{}{
"this":struct{}{},
"that":struct{}{},
"these":struct{}{},
"those":struct{}{},
},
ExpectedBits: []string{
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "/apple/banana/cherry",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"/apple/banana/cherry",
},
},
{
UncompiledPattern: "/apple/banana/cherry/",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"/apple/banana/cherry/",
},
},
{
UncompiledPattern: "//apple/banana/cherry",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"//apple/banana/cherry",
},
},
{
UncompiledPattern: "/apple//banana/cherry",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"/apple//banana/cherry",
},
},
{
UncompiledPattern: "/apple/banana/cherry//",
ExpectedNames:[]string{},
ExpectedNamesSet:map[string]struct{}{},
ExpectedBits: []string{
"/apple/banana/cherry//",
},
},
{
UncompiledPattern: "/apple/{banana}",
ExpectedNames:[]string{ "banana"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
},
},
{
UncompiledPattern: "/apple/{banana}/",
ExpectedNames:[]string{ "banana"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"/",
},
},
{
UncompiledPattern: "/apple/{banana}//",
ExpectedNames:[]string{ "banana"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"//",
},
},
{
UncompiledPattern: "/apple/{banana}/cherry",
ExpectedNames:[]string{ "banana"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"/cherry",
},
},
{
UncompiledPattern: "/apple/{banana}/cherry/",
ExpectedNames:[]string{ "banana"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"/cherry/",
},
},
{
UncompiledPattern: "/apple/{banana}/cherry/{grape}",
ExpectedNames:[]string{ "banana", "grape"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
"grape":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"/cherry/",
wildcardBit,
},
},
{
UncompiledPattern: "/apple/{banana}/cherry/{grape}/",
ExpectedNames:[]string{ "banana", "grape"},
ExpectedNamesSet:map[string]struct{}{
"banana":struct{}{},
"grape":struct{}{},
},
ExpectedBits: []string{
"/apple/",
wildcardBit,
"/cherry/",
wildcardBit,
"/",
},
},
}
for testNumber, test := range tests {
actualPattern, err := Compile(test.UncompiledPattern)
if nil != err {
t.Errorf("For test #%d, did not expect to receive an error, but actually got one: %v\nPATTERN: %q", testNumber, err, test.UncompiledPattern)
continue
}
if expected, actual := test.ExpectedBits, actualPattern.(*internalPattern).bits; len(expected) != len(actual) {
t.Errorf("For test #%d, expected compiled pattern to have %d bits, but actually had %d.\nEXPECTED BITS: %#v\nACTUAL BITS: %#v\nPATTERN: %q", testNumber, len(expected), len(actual), expected, actual, test.UncompiledPattern)
continue
} else {
for bitNumber, expectedBit := range expected {
actualBit := actual[bitNumber]
if expectedBit != actualBit {
t.Errorf("For test #%d and bit #%d, expected %q, but actually got %q.\nEXPECTED BITS: %#v\nACTUAL BITS: %#v\nPATTERN: %q", testNumber, bitNumber, expectedBit, actualBit, expected, actual, test.UncompiledPattern)
continue
}
}
}
if expected, actual := test.ExpectedNames, actualPattern.MatchNames(); len(expected) != len(actual) {
t.Errorf("For test #%d, when checking directly, expected compiled pattern to have %d names, but actually had %d.\nEXPECTED NAMES: %#v\nACTUAL NAMES: %#v\nPATTERN: %q", testNumber, len(expected), len(actual), expected, actual, test.UncompiledPattern)
continue
} else {
for nameNumber, expectedName := range expected {
actualName := actual[nameNumber]
if expectedName != actualName {
t.Errorf("For test #%d and name #%d, expected %q, but actually got %q.\nEXPECTED NAMES: %#v\nACTUAL NAMES: %#v\nPATTERN: %q", testNumber, nameNumber, expectedName, actualName, expected, actual, test.UncompiledPattern)
continue
}
}
}
if expected, actual := test.ExpectedNamesSet, actualPattern.(*internalPattern).namesSet; len(expected) != len(actual) {
t.Errorf("For test #%d, when checking directly, expected compiled pattern to have %d names in set, but actually had %d.\nEXPECTED NAMES SET: %#v\nACTUAL NAMES SET: %#v\nPATTERN: %q", testNumber, len(expected), len(actual), expected, actual, test.UncompiledPattern)
continue
} else {
for expectedName, _ := range expected {
_, ok := actual[expectedName]
if !ok {
t.Errorf("For test #%d, expected name %q to exist, but actually did't.\nEXPECTED NAMES SET: %#v\nACTUAL NAMES SET: %#v\nPATTERN: %q", testNumber, expectedName, expected, actual, test.UncompiledPattern)
continue
}
}
}
if expected, actual := len(test.ExpectedNames), len(actualPattern.MatchNames()); expected != actual {
t.Errorf("For test #%d, when checking using .MatchNames(), expected compiled pattern to have %d names, but actually had %d.\nEXPECTED NAMES: %#v\nACTUAL NAMES: %#v\nPATTERN: %q", testNumber, expected, actual, test.ExpectedNames, actualPattern.MatchNames(), test.UncompiledPattern)
continue
}
}
}
func TestCompileFail(t *testing.T) {
tests := []struct{
UncompiledPattern string
ExpectedError string
}{
{
UncompiledPattern: "/users/{userId",
ExpectedError: `Bad Request: Syntax Error: Missing ending "}" (to match beginning "{").`,
},
{
UncompiledPattern: "/users/{userId}/vehicles/{vehicleId",
ExpectedError: `Bad Request: Syntax Error: Missing ending "}" (to match beginning "{").`,
},
{
UncompiledPattern: "/users/{userId/vehicles/{vehicleId}",
ExpectedError: `Bad Request: Syntax Error: "/" inside of "{...}".`,
},
{
UncompiledPattern: "/users/{userId{vehicleId}",
ExpectedError: `Bad Request: Syntax Error: "{" inside of "{...}".`,
},
{
UncompiledPattern: "/apple/{fruitId}/banana/{fruitId}",
ExpectedError: `Bad Request: Syntax Error: Duplicate match name: "fruitId".`,
},
{
UncompiledPattern: "/fruit/{apple/banana/cherry}",
ExpectedError: `Bad Request: Syntax Error: "/" inside of "{...}".`,
},
}
for testNumber, test := range tests {
_, err := Compile(test.UncompiledPattern)
if nil == err {
t.Errorf("For test #%d, expected to receive an error, but actually did not get one: %v\nPATTERN: %q", testNumber, err, test.UncompiledPattern)
continue
}
if expected, actual := test.ExpectedError, err.Error(); expected != actual {
t.Errorf("For test #%d, expected error message to be %q, but actually was %q.\nPATTERN: %q", testNumber, expected, actual, test.UncompiledPattern)
continue
}
}
}

62
doc.go 100644
View File

@ -0,0 +1,62 @@
/*
Package pathmatch provides pattern matching for paths.
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).
The matches can be loaded into variables (when using pathmatch.Match());
or can be loaded into a struct (when using pathmatch.Pattern.MatchAndLoad()).
Example Usage:
pattern, err := pathmatch.Compile("/users/{user_id}/vehicles/{vehicle_id}}")
if nil != err {
//@TODO
}
var userId string
var vehicleId string
didMatch, err := pattern.Match("/users/bMM_kJFMEV/vehicles/o_bcU.RZGK", &userId, &vehicleId)
if nil != err {
//@TODO
}
if didMatch {
fmt.Println("The path matched!")
fmt.Printf("user_id = %q \n", userId) // user_id = "bMM_kJFMEV"
fmt.Printf("vehicle_id = %q \n", vehicleId) // vehicle_id = "o_bcU.RZGK"
} else {
fmt.Println("The patch did not match.")
}
Alternate Example Usage:
pattern, err := pathmatch.Compile("/users/{user_id}/vehicles/{vehicle_id}}")
if nil != err {
//@TODO
}
data := struct{
UserId string `match:"user_id"`
VehicleId string `match:"vehicle_id"`
}{}
didMatch, err := pattern.MatchAndLoad("/users/bMM_kJFMEV/vehicles/o_bcU.RZGK", &data)
if nil != err {
//@TODO
}
if didMatch {
fmt.Println("The path matched!")
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"
} else {
fmt.Println("The patch did not match.")
}
*/
package pathmatch

View File

@ -0,0 +1,57 @@
package pathmatch
import (
"bytes"
"fmt"
)
const (
internalErrorMessagePrefix = "Internal Error: "
)
type InternalErrorComplainer interface {
error
InternalErrorComplainer()
}
// internalInternalErrorComplainer is the only underlying implementation that fits the
// InternalErrorComplainer interface, in this library.
type internalInternalErrorComplainer struct {
msg string
}
// newInternalErrorComplainer creates a new internalInternalErrorComplainer (struct) and
// returns it as a InternalErrorComplainer (interface).
func newInternalErrorComplainer(format string, a ...interface{}) InternalErrorComplainer {
msg := fmt.Sprintf(format, a...)
err := internalInternalErrorComplainer{
msg:msg,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the InternalErrorComplainer
// interface).
func (err *internalInternalErrorComplainer) Error() string {
var buffer bytes.Buffer
buffer.WriteString(internalErrorMessagePrefix)
buffer.WriteString(err.msg)
return buffer.String()
}
// InternalErrorComplainer method is necessary to satisfy the 'InternalErrorComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalInternalErrorComplainer) InternalErrorComplainer() {
// Nothing here.
}

View File

@ -0,0 +1,69 @@
package pathmatch
import (
"fmt"
)
type NotEnoughArgumentsComplainer interface {
BadRequestComplainer
NotEnoughArgumentsComplainer()
ExpectedAtLeast() int
Actual() int
}
// internalNotEnoughArgumentsComplainer is the only underlying implementation that fits the
// NotEnoughArgumentsComplainer interface, in this library.
type internalNotEnoughArgumentsComplainer struct {
expectedAtLeast int
actual int
}
// newNotEnoughArgumentsComplainer creates a new internalNotEnoughArgumentsComplainer (struct) and
// returns it as a NotEnoughArgumentsComplainer (interface).
func newNotEnoughArgumentsComplainer(expectedAtLeast int, actual int) NotEnoughArgumentsComplainer {
err := internalNotEnoughArgumentsComplainer{
expectedAtLeast:expectedAtLeast,
actual:actual,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the
// NotEnoughArgumentsComplainer interface).
func (err *internalNotEnoughArgumentsComplainer) Error() string {
plural := ""
if 1 < err.expectedAtLeast {
plural = "s"
}
return fmt.Sprintf("Bad Request: Not enough arguments. Expected at least %d argument%s, but actually got %d.", err.expectedAtLeast, plural, err.actual)
}
// BadRequestComplainer method is necessary to satisfy the 'BadRequestComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalNotEnoughArgumentsComplainer) BadRequestComplainer() {
// Nothing here.
}
// NotEnoughArgumentsComplainer method is necessary to satisfy the 'NotEnoughArgumentsComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalNotEnoughArgumentsComplainer) NotEnoughArgumentsComplainer() {
// Nothing here.
}
func (err *internalNotEnoughArgumentsComplainer) ExpectedAtLeast() int {
return err.expectedAtLeast
}
func (err *internalNotEnoughArgumentsComplainer) Actual() int {
return err.actual
}

65
pattern.go 100644
View File

@ -0,0 +1,65 @@
package pathmatch
// Pattern represents a compiled pattern. It is what is returned
// from calling either the Compile to MustCompile funcs.
//
// Pattern provides the Match, MatchAndLoad, and MatchNames methods.
//
// Example Usage:
//
// pattern, err := pathmath.Compile("/users/{user_id}")
// if nil != err {
// fmt.Printf("ERROR Compiling: %v\n", err)
// return
// }
//
// var userId string
//
// didMatch, err := pattern.Match("/users/123", userId)
// if nil != err {
// fmt.Printf("ERROR Matching: %v\n", err)
// return
// }
//
// if didMatch {
// fmt.Printf("user_id = %q\n", userId)
// } else {
// fmt.Println("Did not match.")
// }
type Pattern interface {
Match(string, ...interface{}) (bool, error)
MatchAndLoad(string, interface{}) (bool, error)
MatchNames() []string
}
type internalPattern struct {
bits []string
names []string
namesSet map[string]struct{}
fieldTagName string
}
func (pattern *internalPattern) MatchNames() []string {
return pattern.names
}
func newPattern(fieldTagName string) *internalPattern {
bits := []string{}
names := []string{}
namesSet := map[string]struct{}{}
pattern := internalPattern{
bits:bits,
names:names,
namesSet:namesSet,
fieldTagName:fieldTagName,
}
return &pattern
}

99
pattern_load.go 100644
View File

@ -0,0 +1,99 @@
package pathmatch
import (
"errors"
"fmt"
"reflect"
"strings"
)
var (
errExpectedAPointerToAStruct = newUnsupportedArgumentTypeComplainer("Expected a pointer to a struct, but wasn't.")
)
func (pattern *internalPattern) MatchAndLoad(path string, strct interface{}) (bool, error) {
//@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(pattern.MatchNames())
for i:=0; i<numNames; i++ {
args = append(args, new(string))
}
didMatch, err := pattern.Match(path, args...)
if nil != err {
return doesNotMatter, err
}
if !didMatch {
return false, nil
}
reflectedValue := reflect.ValueOf(strct)
if reflect.Ptr != reflectedValue.Kind() {
return doesNotMatter, errExpectedAPointerToAStruct
}
reflectedValueElem := reflectedValue.Elem()
reflectedValueElemType := reflectedValueElem.Type()
numFields := reflectedValueElemType.NumField()
for fieldNumber:=0; fieldNumber<numFields; fieldNumber++ {
//field := reflectedValueElemType.Field(fieldNumber)
//fieldTag := field.Tag
//name := fieldTag.Get(pattern.fieldTagName)
value := *(args[fieldNumber].(*string))
err := func(rValue reflect.Value, value string, matchName string) (err error) {
defer func() {
if r := recover(); nil != r {
// See if we received a message of the form:
//
// reflect.Set: value of type ??? is not assignable to type ???
//
// If we did then we interpret this as the programmer using this
// trying to load into a struct field of the wrong type.
//
// We return a special error for that.
if s, ok := r.(string); ok {
needle := "reflect.Set: value of type "
if strings.HasPrefix(s, needle) {
needle = " is not assignable to type "
if strings.Contains(s, needle) {
err = newStructFieldWrongTypeComplainer(matchName)
return
}
}
}
msg := fmt.Sprintf("%T %v", r)
err = errors.New( msg )
return
}
}()
rValue.Set( reflect.ValueOf(value) )
return nil
}(reflectedValueElem.Field(fieldNumber), value, pattern.fieldTagName)
if nil != err {
return doesNotMatter, err
}
}
return true, nil
}

View File

@ -0,0 +1,168 @@
package pathmatch
import (
"github.com/fatih/structs"
"testing"
)
func TestMatchAndLoad(t *testing.T) {
tests := []struct{
Pattern Pattern
StructPtr interface{}
Path string
Expected map[string]string
}{
{
Pattern: MustCompile("/{this}/{that}/{these}/{those}"),
StructPtr: new(struct{
This string `match:"this"`
That string `match:"that"`
These string `match:"these"`
Those string `match:"those"`
}),
Path: "/apple/banana/cherry/grape",
Expected: map[string]string{
"This":"apple",
"That":"banana",
"These":"cherry",
"Those":"grape",
},
},
{
Pattern: MustCompile("/user/{sessionKey}"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
{
Pattern: MustCompile("/user/{sessionKey}/"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/DEFAULT"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
{
Pattern: MustCompile("/user/{sessionKey}/vehicle/DEFAULT/"),
StructPtr: new(struct{
SessionKey string `match:"sessionKey"`
}),
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT/",
Expected: map[string]string{
"SessionKey":"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
},
},
/*
{
Pattern: 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}/"),
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}"),
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}/"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/N9Z_tiv7/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "N9Z_tiv7"},
},
*/
}
for testNumber, test := range tests {
for structFieldName, structFieldValue := range structs.Map(test.StructPtr) {
structFieldValueString, ok := structFieldValue.(string)
if !ok {
t.Errorf("For test #%d, expected test.StructPtr.%s to be of type string, but actually was %T.", testNumber, structFieldName, structFieldValue)
continue
}
if expected, actual := "", structFieldValueString; expected != actual {
t.Errorf("For test #%d, expected test.StructPtr.%s to (initially) be %q, but actually was %q.", testNumber, structFieldName, expected, actual)
continue
}
}
if didMatch, err := test.Pattern.MatchAndLoad(test.Path, test.StructPtr); nil != err {
t.Errorf("For test #%d, did not expected an error, but actually got one: %v", testNumber, err)
continue
} else if !didMatch {
t.Errorf("For test #%d, expected path to match pattern, but it didn't.", testNumber)
continue
}
for structFieldName, structFieldValue := range structs.Map(test.StructPtr) {
structFieldValueString, ok := structFieldValue.(string)
if !ok {
t.Errorf("For test #%d, expected test.StructPtr.%s to be of type string, but actually was %T.", testNumber, structFieldName, structFieldValue)
continue
}
if expected, actual := test.Expected[structFieldName], structFieldValueString; expected != actual {
t.Errorf("For test #%d, expected test.StructPtr.%s to (initially) be %q, but actually was %q.", testNumber, structFieldName, expected, actual)
continue
}
}
}
}

56
pattern_match.go 100644
View File

@ -0,0 +1,56 @@
package pathmatch
import (
"strings"
)
const (
doesNotMatter = false
)
var (
errThisShouldNeverHappen = newInternalErrorComplainer("This should never happen.")
)
func (pattern *internalPattern) Match(path string, args ...interface{}) (bool, error) {
s := path
argsIndex := 0
for _, bit := range pattern.bits {
switch bit {
default:
if !strings.HasPrefix(s, bit) {
return false, nil
}
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
}
}
}
return true, nil
}

View File

@ -0,0 +1,129 @@
package pathmatch
import (
"testing"
)
func TestMatch(t *testing.T) {
tests := []struct{
Pattern Pattern
Args []interface{}
Path string
ExpectedArgs []string
}{
{
Pattern: 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"},
},
{
Pattern: MustCompile("/user/{sessionKey}"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: MustCompile("/user/{sessionKey}/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: 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/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: 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/"),
Args: []interface{}{new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/DEFAULT/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij"},
},
{
Pattern: 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}/"),
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}"),
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}/"),
Args: []interface{}{new(string), new(string), },
Path: "/user/76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij/vehicle/N9Z_tiv7/",
ExpectedArgs: []string{"76M6.mXQfgiGSC_YJ5uXSnWUmELbe8OgOm5n.iZ98Ij", "N9Z_tiv7"},
},
}
for testNumber, test := range tests {
for argNumber, arg := range test.Args {
argStringPtr, ok := arg.(*string)
if !ok {
t.Errorf("For test #%d, expected test.Args[%d] to be of type *string, but actually was %T.", testNumber, argNumber, arg)
continue
}
if expected, actual := "", *argStringPtr; expected != actual {
t.Errorf("For test #%d, expected *test.Args[%d] to (initially) be %q, but actually was %q.", testNumber, argNumber, expected, actual)
continue
}
}
if didMatch, err := test.Pattern.Match(test.Path, test.Args...); nil != err {
t.Errorf("For test #%d, did not expected an error, but actually got one: %v", testNumber, err)
continue
} else if !didMatch {
t.Errorf("For test #%d, expected path to match pattern, but it didn't.", testNumber)
continue
}
for argNumber, arg := range test.Args {
argStringPtr, ok := arg.(*string)
if !ok {
t.Errorf("For test #%d, expected test.Args[%d] to be of type *string, but actually was %T.", testNumber, argNumber, arg)
continue
}
if expected, actual := test.ExpectedArgs[argNumber], *argStringPtr; expected != actual {
t.Errorf("For test #%d, expected *test.Args[%d] to be %q, but actually was %q.", testNumber, argNumber, expected, actual)
continue
}
}
}
}

View File

@ -0,0 +1,85 @@
package pathmatch
import (
"fmt"
)
// PatternSyntaxErrorComplainer is used to represent a specific kind of BadRequestComplainer error.
// Specifically, it represents a syntax error in the uncompiled pattern passed to the
// pathmatch.Compile() func.
//
// Example usage is as follows:
//
// pattern, err := pathmatch.Compile("/something/{there_is_a_syntax_error_in_this_pattern")
// if nil != err {
// switch err.(type) {
//
// case pathmatch.PatternSyntaxErrorComplainer: // ← Here we are detecting if the error returned was due to a syntax error, in the uncompiled pattern. Also note that it comes BEFORE the 'pathmatch.BadRequestComplainer' case; THAT IS IMPORTANT!
//
// fmt.Printf("The uncompiled pattern passed to pathmatch.Compile() had a syntax error in it. The error message describing the syntax error is....\n%s\n", err.Error())
// return
//
// case pathmatch.BadRequestComplainer:
//
// fmt.Printf("Something you did when you called pathmatch.Compile() caused an error. The error message was....\n%s\n", err.Error())
// return
//
// case pathmatch.InternalErrorComplainer:
//
// fmt.Printf("It's not your fault; it's our fault. Something bad happened internally when pathmatch.Compile() was running. The error message was....\n%s\n", err.Error())
// return
//
// default:
//
// fmt.Printf("Some kind of unexpected error happend: %v", err)
// return
// }
// }
type PatternSyntaxErrorComplainer interface {
BadRequestComplainer
PatternSyntaxErrorComplainer()
}
// internalPatternSyntaxErrorComplainer is the only underlying implementation that fits the
// PatternSyntaxErrorComplainer interface, in this library.
type internalPatternSyntaxErrorComplainer struct {
msg string
}
// newPatternSyntaxErrorComplainer creates a new internalPatternSyntaxErrorComplainer (struct) and
// returns it as a PatternSyntaxErrorComplainer (interface).
func newPatternSyntaxErrorComplainer(format string, a ...interface{}) PatternSyntaxErrorComplainer {
msg := fmt.Sprintf(format, a...)
err := internalPatternSyntaxErrorComplainer{
msg:msg,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the
// PatternSyntaxErrorComplainer interface).
func (err *internalPatternSyntaxErrorComplainer) Error() string {
s := fmt.Sprintf("Bad Request: Syntax Error: %s", err.msg)
return s
}
// BadRequestComplainer method is necessary to satisfy the 'InternalErrorComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalPatternSyntaxErrorComplainer) BadRequestComplainer() {
// Nothing here.
}
// PatternSyntaxErrorComplainer method is necessary to satisfy the 'PatternSyntaxErrorComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalPatternSyntaxErrorComplainer) PatternSyntaxErrorComplainer() {
// Nothing here.
}

View File

@ -0,0 +1,61 @@
package pathmatch
import (
"fmt"
)
type ScanErrorComplainer interface {
InternalErrorComplainer
ScanErrorComplainer()
WrappedError() error
}
// internalScanErrorComplainer is the only underlying implementation that fits the
// ScanErrorComplainer interface, in this library.
type internalScanErrorComplainer struct {
wrappedError error
argumentIndex int
argumentType string
}
// newScanErrorComplainer creates a new internalScanErrorComplainer (struct) and
// returns it as a ScanErrorComplainer (interface).
func newScanErrorComplainer(wrappedError error, argumentIndex int, argumentType string) ScanErrorComplainer {
err := internalScanErrorComplainer{
wrappedError:wrappedError,
argumentIndex:argumentIndex,
argumentType:argumentType,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the
// ScanErrorComplainer interface).
func (err *internalScanErrorComplainer) Error() string {
s := fmt.Sprintf("Internal Error: Received scan error for argument #%d (%s): %q", err.argumentIndex, err.argumentType, err.wrappedError.Error())
return s
}
// InternalErrorComplainer method is necessary to satisfy the 'InternalErrorComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalScanErrorComplainer) InternalErrorComplainer() {
// Nothing here.
}
// ScanErrorComplainer method is necessary to satisfy the 'ScanErrorComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalScanErrorComplainer) ScanErrorComplainer() {
// Nothing here.
}
func (err *internalScanErrorComplainer) WrappedError() error {
return err.wrappedError
}

37
set.go 100644
View File

@ -0,0 +1,37 @@
package pathmatch
import (
"database/sql"
"fmt"
)
func set(value string, argsIndex int, args ...interface{}) error {
if 0 > argsIndex {
return newInternalErrorComplainer("Index value %d is less than zero.", argsIndex)
}
if lenArgs := len(args); argsIndex >= lenArgs {
expectedAtLeast := 1+argsIndex
actual := lenArgs
return newNotEnoughArgumentsComplainer(expectedAtLeast, actual)
}
arg := args[argsIndex]
switch a := arg.(type) {
case sql.Scanner:
if err := a.Scan(value); nil != err {
return newScanErrorComplainer(err, argsIndex, fmt.Sprintf("%T", arg))
}
case *string:
*a = value
default:
return newUnsupportedIndexedArgumentTypeComplainer(argsIndex, fmt.Sprintf("%T", arg))
}
return nil
}

376
set_test.go 100644
View File

@ -0,0 +1,376 @@
package pathmatch
import (
"math/rand"
"time"
"testing"
)
func TestSetStringPointers(t *testing.T) {
randomness := rand.New(rand.NewSource( time.Now().UTC().UnixNano() ))
runes := []rune(".0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
lenRunes := len(runes)
randomString := func() string {
length := 1 + randomness.Intn(59)
rs := make([]rune, length)
for i := range rs {
rs[i] = runes[randomness.Intn(lenRunes)]
}
return string(rs)
}
tests := []struct{
Value string
ArgsIndex int
Args []interface{}
}{
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ new(string), },
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ new(string), new(string), },
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ new(string), new(string), },
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ new(string), new(string), new(string), },
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ new(string), new(string), new(string), },
},
{
Value: randomString(),
ArgsIndex: 2,
Args: []interface{}{ new(string), new(string), new(string), },
},
}
for testNumber, test := range tests {
for argNumber, arg := range test.Args {
if argStringPtr, ok := arg.(*string); !ok {
t.Errorf("For test #%d, expected test.Args[%d] to be type *string (pointer to string), but actually was: %T", testNumber, argNumber, arg)
continue
} else if argString := *argStringPtr; "" != argString {
t.Errorf("For test #%d, expected test.Args[%d] to be \"\" (empty string), but actually was: %q", testNumber, argNumber, argString)
continue
}
}
if err := set(test.Value, test.ArgsIndex, test.Args...); nil != err {
t.Errorf("For test #%d, did not expected error when calling set(), but actually received one: %v", testNumber, err)
continue
}
for argNumber, arg := range test.Args {
if argNumber == test.ArgsIndex {
continue
}
if argStringPtr, ok := arg.(*string); !ok {
t.Errorf("For test #%d, expected test.Args[%d] to be type *string (pointer to string), but actually was: %T", testNumber, argNumber, arg)
continue
} else if argString := *argStringPtr; "" != argString {
t.Errorf("For test #%d, expected test.Args[%d] to be \"\" (empty string), but actually was: %q", testNumber, argNumber, argString)
continue
}
}
if actualPtr, ok := test.Args[test.ArgsIndex].(*string); !ok {
t.Errorf("For test #%d, expected test.Args[%d] to be type *string (pointer to string), but actually was: %T", testNumber, test.ArgsIndex, test.Args[test.ArgsIndex])
continue
} else if expected, actual := test.Value, *actualPtr; expected != actual {
t.Errorf("For test #%d, expected test.Args[%d] to be type %q, but actually was %q.", testNumber, test.ArgsIndex, expected, actual)
continue
}
}
}
func TestSetFail(t *testing.T) {
const internalError = "internal_error"
const notEnoughArguments = "not_enough_arguments"
const unsupportedArgumentType = "unsupported_argument_type"
randomness := rand.New(rand.NewSource( time.Now().UTC().UnixNano() ))
runes := []rune(".0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
lenRunes := len(runes)
randomString := func() string {
length := 1 + randomness.Intn(59)
rs := make([]rune, length)
for i := range rs {
rs[i] = runes[randomness.Intn(lenRunes)]
}
return string(rs)
}
tests := []struct{
Value string
ArgsIndex int
Args []interface{}
ExpectedFit string
ExpectedErrorString string
}{
{
Value: randomString(),
ArgsIndex: -1,
Args: []interface{}{ new(string), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -1 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: -1,
Args: []interface{}{ randomString(), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -1 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: -2,
Args: []interface{}{ new(string), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -2 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: -2,
Args: []interface{}{ randomString(), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -2 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: -3,
Args: []interface{}{ new(string), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -3 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: -3,
Args: []interface{}{ randomString(), },
ExpectedFit: internalError,
ExpectedErrorString: "Internal Error: Index value -3 is less than zero.",
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #0 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ randomString(), randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #0 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ randomString(), randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #1 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #0 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #1 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 2,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: unsupportedArgumentType,
ExpectedErrorString: "Bad Request: Type of argument #2 (string) is unsupported. However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?",
},
{
Value: randomString(),
ArgsIndex: 0,
Args: []interface{}{ },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 1 argument, but actually got 0.",
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 2 arguments, but actually got 0.",
},
{
Value: randomString(),
ArgsIndex: 2,
Args: []interface{}{ },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 3 arguments, but actually got 0.",
},
{
Value: randomString(),
ArgsIndex: 1,
Args: []interface{}{ randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 2 arguments, but actually got 1.",
},
{
Value: randomString(),
ArgsIndex: 2,
Args: []interface{}{ randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 3 arguments, but actually got 1.",
},
{
Value: randomString(),
ArgsIndex: 3,
Args: []interface{}{ randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 4 arguments, but actually got 1.",
},
{
Value: randomString(),
ArgsIndex: 2,
Args: []interface{}{ randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 3 arguments, but actually got 2.",
},
{
Value: randomString(),
ArgsIndex: 3,
Args: []interface{}{ randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 4 arguments, but actually got 2.",
},
{
Value: randomString(),
ArgsIndex: 4,
Args: []interface{}{ randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 5 arguments, but actually got 2.",
},
{
Value: randomString(),
ArgsIndex: 3,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 4 arguments, but actually got 3.",
},
{
Value: randomString(),
ArgsIndex: 4,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 5 arguments, but actually got 3.",
},
{
Value: randomString(),
ArgsIndex: 5,
Args: []interface{}{ randomString(), randomString(), randomString(), },
ExpectedFit: notEnoughArguments,
ExpectedErrorString: "Bad Request: Not enough arguments. Expected at least 6 arguments, but actually got 3.",
},
}
for testNumber, test := range tests {
if err := set(test.Value, test.ArgsIndex, test.Args...); nil == err {
t.Errorf("For test #%d, expected error when calling set(), but did not actually received one: %v", testNumber, err)
continue
} else {
switch err.(type) {
case InternalErrorComplainer:
if expected, actual := internalError, test.ExpectedFit; expected != actual {
t.Errorf("For test #%d, did indeed expect an error, but did not expect it to fit the \"InternalErrorComplainer\" interface, but actually did: %T.", testNumber, err)
continue
}
case NotEnoughArgumentsComplainer:
if expected, actual := notEnoughArguments, test.ExpectedFit; expected != actual {
t.Errorf("For test #%d, did indeed expect an error, but did not expect it to fit the \"NotEnoughArgumentsComplainer\" interface, but actually did: %T.", testNumber, err)
continue
}
case UnsupportedArgumentTypeComplainer:
if expected, actual := unsupportedArgumentType, test.ExpectedFit; expected != actual {
t.Errorf("For test #%d, did indeed expect an error, but did not expect it to fit the \"UnsupportedArgumentTypeComplainer\" interface, but actually did: %T.", testNumber, err)
continue
}
default:
t.Errorf("For test #%d, did indeed expect an error, but did not expect this error: [%v] (%T).", testNumber, err, err)
continue
}
if expected, actual := test.ExpectedErrorString, err.Error(); expected != actual {
t.Errorf("For test #%d, expected error string to be %q, but actually got %q.", testNumber, expected, actual)
continue
}
}
}
}

View File

@ -0,0 +1,57 @@
package pathmatch
import (
"bytes"
"fmt"
)
const structFieldWrongTypeMessagePrefix = "Bad Request: Wrong type for match "
type StructFieldWrongTypeComplainer interface {
BadRequestComplainer
MatchName() string
}
// internalStructFieldWrongTypeComplainer is the only underlying implementation that fits the
// StructFieldWrongTypeComplainer interface, in this library.
type internalStructFieldWrongTypeComplainer struct {
matchName string
}
// newStructFieldWrongTypeComplainer creates a new internalStructFieldWrongTypeComplainer (struct) and
// returns it as a StructFieldWrongTypeComplainer (interface).
func newStructFieldWrongTypeComplainer(matchName string) StructFieldWrongTypeComplainer {
err := internalStructFieldWrongTypeComplainer{
matchName:matchName,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the StructFieldWrongTypeComplainer
// interface).
func (err *internalStructFieldWrongTypeComplainer) Error() string {
var buffer bytes.Buffer
buffer.WriteString(structFieldWrongTypeMessagePrefix)
buffer.WriteString(fmt.Sprintf("%q", err.matchName))
return buffer.String()
}
// BadRequestComplainer method is necessary to satisfy the 'BadRequestComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalStructFieldWrongTypeComplainer) BadRequestComplainer() {
// Nothing here.
}
// DependencyName method is necessary to satisfy the 'StructFieldWrongTypeComplainer' interface.
func (err *internalStructFieldWrongTypeComplainer) MatchName() string {
return err.matchName
}

View File

@ -0,0 +1,107 @@
package pathmatch
import (
"fmt"
)
type UnsupportedArgumentTypeComplainer interface {
BadRequestComplainer
UnsupportedArgumentTypeComplainer()
}
// internalUnsupportedIndexedArgumentTypeComplainer is the only underlying implementation that fits the
// UnsupportedArgumentTypeComplainer interface, in this library.
type internalUnsupportedIndexedArgumentTypeComplainer struct {
argumentIndex int
argumentType string
}
// newUnsupportedIndexedArgumentTypeComplainer creates a new internalUnsupportedIndexedArgumentTypeComplainer (struct) and
// returns it as a UnsupportedArgumentTypeComplainer (interface).
func newUnsupportedIndexedArgumentTypeComplainer(argumentIndex int, argumentType string) UnsupportedArgumentTypeComplainer {
err := internalUnsupportedIndexedArgumentTypeComplainer{
argumentIndex:argumentIndex,
argumentType:argumentType,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the
// UnsupportedArgumentTypeComplainer interface).
func (err *internalUnsupportedIndexedArgumentTypeComplainer) Error() string {
s := fmt.Sprintf("Bad Request: Type of argument #%d (%s) is unsupported.", err.argumentIndex, err.argumentType)
if "string" == err.argumentType {
s = fmt.Sprintf("%s However, type \"*string\" (pointer to string) is supported; did you mean to use a \"*string\" instead?", s)
}
return s
}
// BadRequestComplainer method is necessary to satisfy the 'BadRequestComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalUnsupportedIndexedArgumentTypeComplainer) BadRequestComplainer() {
// Nothing here.
}
// UnsupportedArgumentTypeComplainer method is necessary to satisfy the 'UnsupportedArgumentTypeComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalUnsupportedIndexedArgumentTypeComplainer) UnsupportedArgumentTypeComplainer() {
// Nothing here.
}
// internalUnsupportedArgumentTypeComplainer is the only underlying implementation that fits the
// UnsupportedArgumentTypeComplainer interface, in this library.
type internalUnsupportedArgumentTypeComplainer struct {
msg string
}
// newUnsupportedArgumentTypeComplainer creates a new internalUnsupportedArgumentTypeComplainer (struct) and
// returns it as a UnsupportedArgumentTypeComplainer (interface).
func newUnsupportedArgumentTypeComplainer(format string, a ...interface{}) UnsupportedArgumentTypeComplainer {
msg := fmt.Sprintf(format, a...)
err := internalUnsupportedArgumentTypeComplainer{
msg:msg,
}
return &err
}
// Error method is necessary to satisfy the 'error' interface (and the
// UnsupportedArgumentTypeComplainer interface).
func (err *internalUnsupportedArgumentTypeComplainer) Error() string {
return fmt.Sprintf("Bad Request: Unsupported Argument Type: %s", err.msg)
}
// BadRequestComplainer method is necessary to satisfy the 'BadRequestComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalUnsupportedArgumentTypeComplainer) BadRequestComplainer() {
// Nothing here.
}
// UnsupportedArgumentTypeComplainer method is necessary to satisfy the 'UnsupportedArgumentTypeComplainer' interface.
// It exists to make this error type detectable in a Go type-switch.
func (err *internalUnsupportedArgumentTypeComplainer) UnsupportedArgumentTypeComplainer() {
// Nothing here.
}