Compare commits
18 Commits
32aff3e07f
...
3606f9984d
Author | SHA1 | Date |
---|---|---|
Adrien Marquès | 3606f9984d | |
Adrien Marquès | 7b812c6648 | |
Adrien Marquès | dc34d9a81a | |
Adrien Marquès | cdbe4cceac | |
Adrien Marquès | 03d5e87c37 | |
Adrien Marquès | c7aa87c660 | |
Adrien Marquès | 0f62fc25a0 | |
Adrien Marquès | 8c539370aa | |
Adrien Marquès | acd0e73438 | |
Adrien Marquès | b38a9a8111 | |
Adrien Marquès | 93b31b9718 | |
Adrien Marquès | 12417f7f1c | |
Adrien Marquès | e7f10723a6 | |
Adrien Marquès | c32b038da2 | |
Adrien Marquès | 1b4922693b | |
Adrien Marquès | 4e0d669029 | |
Adrien Marquès | 2c1b9cf5ff | |
Adrien Marquès | d1ab4fefb0 |
|
@ -2,15 +2,21 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Error allows you to create constant "const" error with type boxing.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err Error) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrReqParamNotFound is thrown when a request parameter is not found
|
// ErrReqParamNotFound is thrown when a request parameter is not found
|
||||||
const ErrReqParamNotFound = cerr.Error("request parameter not found")
|
const ErrReqParamNotFound = Error("request parameter not found")
|
||||||
|
|
||||||
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
// ErrReqParamNotType is thrown when a request parameter is not asked with the right type
|
||||||
const ErrReqParamNotType = cerr.Error("request parameter does not fulfills type")
|
const ErrReqParamNotType = Error("request parameter does not fulfills type")
|
||||||
|
|
||||||
// RequestParam defines input parameters of an api request
|
// RequestParam defines input parameters of an api request
|
||||||
type RequestParam map[string]interface{}
|
type RequestParam map[string]interface{}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package builtin
|
package builtin
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/config/datatype"
|
import "git.xdrm.io/go/aicra/datatype"
|
||||||
|
|
||||||
// AnyDataType is what its name tells
|
// AnyDataType is what its name tells
|
||||||
type AnyDataType struct{}
|
type AnyDataType struct{}
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAny_AvailableTypes(t *testing.T) {
|
func TestAny_AvailableTypes(t *testing.T) {
|
|
@ -1,6 +1,6 @@
|
||||||
package builtin
|
package builtin
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/config/datatype"
|
import "git.xdrm.io/go/aicra/datatype"
|
||||||
|
|
||||||
// BoolDataType is what its name tells
|
// BoolDataType is what its name tells
|
||||||
type BoolDataType struct{}
|
type BoolDataType struct{}
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBool_AvailableTypes(t *testing.T) {
|
func TestBool_AvailableTypes(t *testing.T) {
|
|
@ -3,7 +3,7 @@ package builtin
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FloatDataType is what its name tells
|
// FloatDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFloat64_AvailableTypes(t *testing.T) {
|
func TestFloat64_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntDataType is what its name tells
|
// IntDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInt_AvailableTypes(t *testing.T) {
|
func TestInt_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
var fixedLengthRegex = regexp.MustCompile(`^string\((\d+)\)$`)
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestString_AvailableTypes(t *testing.T) {
|
func TestString_AvailableTypes(t *testing.T) {
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UintDataType is what its name tells
|
// UintDataType is what its name tells
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUint_AvailableTypes(t *testing.T) {
|
func TestUint_AvailableTypes(t *testing.T) {
|
|
@ -4,9 +4,9 @@ package datatype
|
||||||
// and casts the value into a compatible type
|
// and casts the value into a compatible type
|
||||||
type Validator func(value interface{}) (cast interface{}, valid bool)
|
type Validator func(value interface{}) (cast interface{}, valid bool)
|
||||||
|
|
||||||
// DataType builds a DataType from the type definition (from the
|
// T builds a T from the type definition (from the
|
||||||
// configuration field "type") and returns NIL if the type
|
// configuration field "type") and returns NIL if the type
|
||||||
// definition does not match this DataType
|
// definition does not match this T
|
||||||
type DataType interface {
|
type T interface {
|
||||||
Build(typeDefinition string) Validator
|
Build(typeDefinition string) Validator
|
||||||
}
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
package cerr
|
|
||||||
|
|
||||||
// Error allows you to create constant "const" error with type boxing.
|
|
||||||
type Error string
|
|
||||||
|
|
||||||
// Error implements the error builtin interface.
|
|
||||||
func (err Error) Error() string {
|
|
||||||
return string(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap returns a new error which wraps a new error into itself.
|
|
||||||
func (err Error) Wrap(e error) *WrapError {
|
|
||||||
return &WrapError{
|
|
||||||
base: err,
|
|
||||||
wrap: e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapString returns a new error which wraps a new error created from a string.
|
|
||||||
func (err Error) WrapString(e string) *WrapError {
|
|
||||||
return &WrapError{
|
|
||||||
base: err,
|
|
||||||
wrap: Error(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapError is way to wrap errors recursively.
|
|
||||||
type WrapError struct {
|
|
||||||
base error
|
|
||||||
wrap error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the error builtin interface recursively.
|
|
||||||
func (err *WrapError) Error() string {
|
|
||||||
return err.base.Error() + ": " + err.wrap.Error()
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package cerr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConstError(t *testing.T) {
|
|
||||||
const cerr1 = Error("some-string")
|
|
||||||
const cerr2 = Error("some-other-string")
|
|
||||||
const cerr3 = Error("some-string") // same const value as @cerr1
|
|
||||||
|
|
||||||
if cerr1.Error() == cerr2.Error() {
|
|
||||||
t.Errorf("cerr1 should not be equal to cerr2 ('%s', '%s')", cerr1.Error(), cerr2.Error())
|
|
||||||
}
|
|
||||||
if cerr2.Error() == cerr3.Error() {
|
|
||||||
t.Errorf("cerr2 should not be equal to cerr3 ('%s', '%s')", cerr2.Error(), cerr3.Error())
|
|
||||||
}
|
|
||||||
if cerr1.Error() != cerr3.Error() {
|
|
||||||
t.Errorf("cerr1 should be equal to cerr3 ('%s', '%s')", cerr1.Error(), cerr3.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrappedConstError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
const readErrorConst = Error("cannot read file")
|
|
||||||
var wrappedReadError = parent.Wrap(readErrorConst)
|
|
||||||
|
|
||||||
expectedWrappedReadError := fmt.Sprintf("%s: %s", parent.Error(), readErrorConst.Error())
|
|
||||||
if wrappedReadError.Error() != expectedWrappedReadError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedReadError.Error(), expectedWrappedReadError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestWrappedStandardError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
var writeErrorStandard error = errors.New("cannot write file")
|
|
||||||
var wrappedWriteError = parent.Wrap(writeErrorStandard)
|
|
||||||
|
|
||||||
expectedWrappedWriteError := fmt.Sprintf("%s: %s", parent.Error(), writeErrorStandard.Error())
|
|
||||||
if wrappedWriteError.Error() != expectedWrappedWriteError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedWriteError.Error(), expectedWrappedWriteError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestWrappedStringError(t *testing.T) {
|
|
||||||
const parent = Error("file error")
|
|
||||||
|
|
||||||
var closeErrorString string = "cannot close file"
|
|
||||||
var wrappedCloseError = parent.WrapString(closeErrorString)
|
|
||||||
|
|
||||||
expectedWrappedCloseError := fmt.Sprintf("%s: %s", parent.Error(), closeErrorString)
|
|
||||||
if wrappedCloseError.Error() != expectedWrappedCloseError {
|
|
||||||
t.Errorf("expected '%s' (got '%s')", wrappedCloseError.Error(), expectedWrappedCloseError)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,10 +8,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype/builtin"
|
"git.xdrm.io/go/aicra/datatype/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLegalServiceName(t *testing.T) {
|
func TestLegalServiceName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Raw string
|
Raw string
|
||||||
Error error
|
Error error
|
||||||
|
@ -43,35 +45,35 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
||||||
ErrInvalidPatternBracePosition,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
||||||
ErrInvalidPatternBracePosition,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
||||||
nil,
|
ErrUndefinedBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
||||||
ErrInvalidPatternBracePosition,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
||||||
ErrInvalidPatternBracePosition,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
||||||
nil,
|
ErrUndefinedBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
||||||
ErrInvalidPatternOpeningBrace,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
||||||
ErrInvalidPatternClosingBrace,
|
ErrInvalidPatternBraceCapture,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +101,7 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestAvailableMethods(t *testing.T) {
|
func TestAvailableMethods(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Raw string
|
Raw string
|
||||||
ValidMethod bool
|
ValidMethod bool
|
||||||
|
@ -146,6 +149,7 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestParseEmpty(t *testing.T) {
|
func TestParseEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[]`)
|
reader := strings.NewReader(`[]`)
|
||||||
_, err := Parse(reader)
|
_, err := Parse(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,6 +171,7 @@ func TestParseJsonError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMissingMethodDescription(t *testing.T) {
|
func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Raw string
|
Raw string
|
||||||
ValidDescription bool
|
ValidDescription bool
|
||||||
|
@ -217,6 +222,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
reader := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
@ -233,12 +239,12 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(srv.services) < 1 {
|
if len(srv.Services) < 1 {
|
||||||
t.Errorf("expected a service")
|
t.Errorf("expected a service")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, param := range srv.services[0].Input {
|
for _, param := range srv.Services[0].Input {
|
||||||
if param.Rename != "original" {
|
if param.Rename != "original" {
|
||||||
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -247,6 +253,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestOptionalParam(t *testing.T) {
|
func TestOptionalParam(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
reader := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
|
@ -266,11 +273,11 @@ func TestOptionalParam(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(srv.services) < 1 {
|
if len(srv.Services) < 1 {
|
||||||
t.Errorf("expected a service")
|
t.Errorf("expected a service")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
for pName, param := range srv.services[0].Input {
|
for pName, param := range srv.Services[0].Input {
|
||||||
|
|
||||||
if pName == "optional" || pName == "optional2" {
|
if pName == "optional" || pName == "optional2" {
|
||||||
if !param.Optional {
|
if !param.Optional {
|
||||||
|
@ -288,6 +295,7 @@ func TestOptionalParam(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestParseParameters(t *testing.T) {
|
func TestParseParameters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Raw string
|
Raw string
|
||||||
Error error
|
Error error
|
||||||
|
@ -303,7 +311,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrIllegalParamName,
|
ErrMissingParamDesc,
|
||||||
},
|
},
|
||||||
{ // invalid param name suffix
|
{ // invalid param name suffix
|
||||||
`[
|
`[
|
||||||
|
@ -316,7 +324,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrIllegalParamName,
|
ErrMissingParamDesc,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ // missing param description
|
{ // missing param description
|
||||||
|
@ -473,6 +481,57 @@ func TestParseParameters(t *testing.T) {
|
||||||
]`,
|
]`,
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ // URI parameter
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/{uri}",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"{uri}": { "info": "valid", "type": "any", "name": "freename" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // URI parameter cannot be optional
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/{uri}",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"{uri}": { "info": "valid", "type": "?any", "name": "freename" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrIllegalOptionalURIParam,
|
||||||
|
},
|
||||||
|
{ // URI parameter not specified
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"{uri}": { "info": "valid", "type": "?any", "name": "freename" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrUnspecifiedBraceCapture,
|
||||||
|
},
|
||||||
|
{ // URI parameter not defined
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/{uri}",
|
||||||
|
"info": "info",
|
||||||
|
"in": { }
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrUndefinedBraceCapture,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
@ -500,8 +559,8 @@ func TestParseParameters(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: rewrite with new api format
|
|
||||||
func TestMatchSimple(t *testing.T) {
|
func TestMatchSimple(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Config string
|
Config string
|
||||||
URL string
|
URL string
|
||||||
|
@ -583,7 +642,7 @@ func TestMatchSimple(t *testing.T) {
|
||||||
"path": "/a/{valid}",
|
"path": "/a/{valid}",
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"in": {
|
"in": {
|
||||||
"{id}": {
|
"{valid}": {
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
}
|
}
|
||||||
|
@ -598,7 +657,7 @@ func TestMatchSimple(t *testing.T) {
|
||||||
"path": "/a/{valid}",
|
"path": "/a/{valid}",
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"in": {
|
"in": {
|
||||||
"{id}": {
|
"{valid}": {
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
}
|
}
|
||||||
|
@ -619,14 +678,14 @@ func TestMatchSimple(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(srv.services) != 1 {
|
if len(srv.Services) != 1 {
|
||||||
t.Errorf("expected to have 1 service, got %d", len(srv.services))
|
t.Errorf("expected to have 1 service, got %d", len(srv.Services))
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
|
req := httptest.NewRequest(http.MethodGet, test.URL, nil)
|
||||||
|
|
||||||
match := srv.services[0].Match(req)
|
match := srv.Services[0].Match(req)
|
||||||
if test.Match && !match {
|
if test.Match && !match {
|
||||||
t.Errorf("expected '%s' to match", test.URL)
|
t.Errorf("expected '%s' to match", test.URL)
|
||||||
t.FailNow()
|
t.FailNow()
|
|
@ -23,18 +23,21 @@ const ErrPatternCollision = Error("invalid config format")
|
||||||
// ErrInvalidPattern - a service pattern is malformed
|
// ErrInvalidPattern - a service pattern is malformed
|
||||||
const ErrInvalidPattern = Error("must begin with a '/' and not end with")
|
const ErrInvalidPattern = Error("must begin with a '/' and not end with")
|
||||||
|
|
||||||
// ErrInvalidPatternBracePosition - a service pattern opening/closing brace is not directly between '/'
|
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
|
||||||
const ErrInvalidPatternBracePosition = Error("capturing braces must be alone between slashes")
|
const ErrInvalidPatternBraceCapture = Error("invalid uri capturing braces")
|
||||||
|
|
||||||
// ErrInvalidPatternOpeningBrace - a service pattern opening brace is invalid
|
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
|
||||||
const ErrInvalidPatternOpeningBrace = Error("opening brace already open")
|
const ErrUnspecifiedBraceCapture = Error("capturing brace missing in the path")
|
||||||
|
|
||||||
// ErrInvalidPatternClosingBrace - a service pattern closing brace is invalid
|
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
|
||||||
const ErrInvalidPatternClosingBrace = Error("closing brace already closed")
|
const ErrUndefinedBraceCapture = Error("capturing brace missing input definition")
|
||||||
|
|
||||||
// ErrMissingDescription - a service is missing its description
|
// ErrMissingDescription - a service is missing its description
|
||||||
const ErrMissingDescription = Error("missing description")
|
const ErrMissingDescription = Error("missing description")
|
||||||
|
|
||||||
|
// ErrIllegalOptionalURIParam - an URI parameter cannot be optional
|
||||||
|
const ErrIllegalOptionalURIParam = Error("URI parameter cannot be optional")
|
||||||
|
|
||||||
// ErrMissingParamDesc - a parameter is missing its description
|
// ErrMissingParamDesc - a parameter is missing its description
|
||||||
const ErrMissingParamDesc = Error("missing parameter description")
|
const ErrMissingParamDesc = Error("missing parameter description")
|
||||||
|
|
||||||
|
@ -42,7 +45,7 @@ const ErrMissingParamDesc = Error("missing parameter description")
|
||||||
const ErrUnknownDataType = Error("unknown data type")
|
const ErrUnknownDataType = Error("unknown data type")
|
||||||
|
|
||||||
// ErrIllegalParamName - a parameter has an illegal name
|
// ErrIllegalParamName - a parameter has an illegal name
|
||||||
const ErrIllegalParamName = Error("parameter name must not begin/end with '_'")
|
const ErrIllegalParamName = Error("illegal parameter name")
|
||||||
|
|
||||||
// ErrMissingParamType - a parameter has an illegal type
|
// ErrMissingParamType - a parameter has an illegal type
|
||||||
const ErrMissingParamType = Error("missing parameter type")
|
const ErrMissingParamType = Error("missing parameter type")
|
|
@ -2,8 +2,8 @@ package config
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// splits an URL without empty sets
|
// SplitURL without empty sets
|
||||||
func splitURL(url string) []string {
|
func SplitURL(url string) []string {
|
||||||
trimmed := strings.Trim(url, " /\t\r\n")
|
trimmed := strings.Trim(url, " /\t\r\n")
|
||||||
split := strings.Split(trimmed, "/")
|
split := strings.Split(trimmed, "/")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/config/datatype"
|
import "git.xdrm.io/go/aicra/datatype"
|
||||||
|
|
||||||
func (param *Parameter) checkAndFormat() error {
|
func (param *Parameter) checkAndFormat() error {
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func (param *Parameter) checkAndFormat() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// assigns the first matching data type from the type definition
|
// assigns the first matching data type from the type definition
|
||||||
func (param *Parameter) assignDataType(types []datatype.DataType) bool {
|
func (param *Parameter) assignDataType(types []datatype.T) bool {
|
||||||
for _, dtype := range types {
|
for _, dtype := range types {
|
||||||
param.Validator = dtype.Build(param.Type)
|
param.Validator = dtype.Build(param.Type)
|
||||||
if param.Validator != nil {
|
if param.Validator != nil {
|
|
@ -7,23 +7,23 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse builds a server configuration from a json reader and checks for most format errors.
|
// Parse builds a server configuration from a json reader and checks for most format errors.
|
||||||
// you can provide additional DataTypes as variadic arguments
|
// you can provide additional DataTypes as variadic arguments
|
||||||
func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
types: make([]datatype.DataType, 0),
|
Types: make([]datatype.T, 0),
|
||||||
services: make([]*Service, 0),
|
Services: make([]*Service, 0),
|
||||||
}
|
}
|
||||||
// add data types
|
// add data types
|
||||||
for _, dtype := range dtypes {
|
for _, dtype := range dtypes {
|
||||||
server.types = append(server.types, dtype)
|
server.Types = append(server.Types, dtype)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse JSON
|
// parse JSON
|
||||||
if err := json.NewDecoder(r).Decode(&server.services); err != nil {
|
if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,23 +40,34 @@ func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find a service matching an incoming HTTP request
|
||||||
|
func (server Server) Find(r *http.Request) *Service {
|
||||||
|
for _, service := range server.Services {
|
||||||
|
if matches := service.Match(r); matches {
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// collide returns if there is collision between services
|
// collide returns if there is collision between services
|
||||||
func (server *Server) collide() error {
|
func (server *Server) collide() error {
|
||||||
length := len(server.services)
|
length := len(server.Services)
|
||||||
|
|
||||||
// for each service combination
|
// for each service combination
|
||||||
for a := 0; a < length; a++ {
|
for a := 0; a < length; a++ {
|
||||||
for b := a + 1; b < length; b++ {
|
for b := a + 1; b < length; b++ {
|
||||||
aService := server.services[a]
|
aService := server.Services[a]
|
||||||
bService := server.services[b]
|
bService := server.Services[b]
|
||||||
|
|
||||||
// ignore different method
|
// ignore different method
|
||||||
if aService.Method != bService.Method {
|
if aService.Method != bService.Method {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
aParts := splitURL(aService.Pattern)
|
aParts := SplitURL(aService.Pattern)
|
||||||
bParts := splitURL(bService.Pattern)
|
bParts := SplitURL(bService.Pattern)
|
||||||
|
|
||||||
// not same size
|
// not same size
|
||||||
if len(aParts) != len(bParts) {
|
if len(aParts) != len(bParts) {
|
||||||
|
@ -120,20 +131,9 @@ func (server *Server) collide() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a service matching an incoming HTTP request
|
|
||||||
func (server Server) Find(r *http.Request) *Service {
|
|
||||||
for _, service := range server.services {
|
|
||||||
if service.Match(r) {
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
||||||
func (server Server) checkAndFormat() error {
|
func (server Server) checkAndFormat() error {
|
||||||
for _, service := range server.services {
|
for _, service := range server.Services {
|
||||||
|
|
||||||
// check method
|
// check method
|
||||||
err := service.checkMethod()
|
err := service.checkMethod()
|
||||||
|
@ -154,11 +154,18 @@ func (server Server) checkAndFormat() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check input parameters
|
// check input parameters
|
||||||
err = service.checkAndFormatInput(server.types)
|
err = service.checkAndFormatInput(server.Types)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
|
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fail if a brace capture remains undefined
|
||||||
|
for _, capture := range service.Captures {
|
||||||
|
if capture.Ref == nil {
|
||||||
|
return fmt.Errorf("%s '%s' [in]: %s: %w", service.Method, service.Pattern, capture.Name, ErrUndefinedBraceCapture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -3,11 +3,15 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
|
|
||||||
// Match returns if this service would handle this HTTP request
|
// Match returns if this service would handle this HTTP request
|
||||||
func (svc *Service) Match(req *http.Request) bool {
|
func (svc *Service) Match(req *http.Request) bool {
|
||||||
// method
|
// method
|
||||||
|
@ -21,7 +25,7 @@ func (svc *Service) Match(req *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and extract input
|
// check and extract input
|
||||||
// todo: check if input match
|
// todo: check if input match and extract models
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -50,40 +54,40 @@ func (svc *Service) checkPattern() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check capturing braces
|
// for each slash-separated chunk
|
||||||
depth := 0
|
parts := SplitURL(svc.Pattern)
|
||||||
for c, l := 1, length; c < l; c++ {
|
for i, part := range parts {
|
||||||
char := svc.Pattern[c]
|
if len(part) < 1 {
|
||||||
|
return ErrInvalidPattern
|
||||||
if char == '{' {
|
|
||||||
// opening brace when already opened
|
|
||||||
if depth != 0 {
|
|
||||||
return ErrInvalidPatternOpeningBrace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// not directly preceded by a slash
|
// if brace capture
|
||||||
if svc.Pattern[c-1] != '/' {
|
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
return ErrInvalidPatternBracePosition
|
braceName := matches[0][1]
|
||||||
|
|
||||||
|
// append
|
||||||
|
if svc.Captures == nil {
|
||||||
|
svc.Captures = make([]*BraceCapture, 0)
|
||||||
}
|
}
|
||||||
depth++
|
svc.Captures = append(svc.Captures, &BraceCapture{
|
||||||
|
Index: i,
|
||||||
|
Name: braceName,
|
||||||
|
Ref: nil,
|
||||||
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if char == '}' {
|
|
||||||
// closing brace when already closed
|
// fail on invalid format
|
||||||
if depth != 1 {
|
if strings.ContainsAny(part, "{}") {
|
||||||
return ErrInvalidPatternClosingBrace
|
return ErrInvalidPatternBraceCapture
|
||||||
}
|
|
||||||
// not directly followed by a slash or end of pattern
|
|
||||||
if c+1 < l && svc.Pattern[c+1] != '/' {
|
|
||||||
return ErrInvalidPatternBracePosition
|
|
||||||
}
|
|
||||||
depth--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
func (svc *Service) checkAndFormatInput(types []datatype.T) error {
|
||||||
|
|
||||||
// ignore no parameter
|
// ignore no parameter
|
||||||
if svc.Input == nil || len(svc.Input) < 1 {
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
|
@ -93,12 +97,45 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
||||||
|
|
||||||
// for each parameter
|
// for each parameter
|
||||||
for paramName, param := range svc.Input {
|
for paramName, param := range svc.Input {
|
||||||
|
if len(paramName) < 1 {
|
||||||
// fail on invalid name
|
|
||||||
if strings.Trim(paramName, "_") != paramName {
|
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fail if brace capture does not exists in pattern
|
||||||
|
iscapture := false
|
||||||
|
if matches := braceRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
braceName := matches[0][1]
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, capture := range svc.Captures {
|
||||||
|
if capture.Name == braceName {
|
||||||
|
capture.Ref = param
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture)
|
||||||
|
}
|
||||||
|
iscapture = true
|
||||||
|
|
||||||
|
} else if matches := queryRegex.FindAllStringSubmatch(paramName, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
||||||
|
|
||||||
|
queryName := matches[0][1]
|
||||||
|
|
||||||
|
// init map
|
||||||
|
if svc.Query == nil {
|
||||||
|
svc.Query = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Query[queryName] = param
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if svc.Form == nil {
|
||||||
|
svc.Form = make(map[string]*Parameter)
|
||||||
|
}
|
||||||
|
svc.Form[paramName] = param
|
||||||
|
}
|
||||||
|
|
||||||
// use param name if no rename
|
// use param name if no rename
|
||||||
if len(param.Rename) < 1 {
|
if len(param.Rename) < 1 {
|
||||||
param.Rename = paramName
|
param.Rename = paramName
|
||||||
|
@ -109,6 +146,11 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// capture parameter cannot be optional
|
||||||
|
if iscapture && param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam)
|
||||||
|
}
|
||||||
|
|
||||||
if !param.assignDataType(types) {
|
if !param.assignDataType(types) {
|
||||||
return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType)
|
return fmt.Errorf("%s: %w", paramName, ErrUnknownDataType)
|
||||||
}
|
}
|
||||||
|
@ -136,8 +178,8 @@ func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
||||||
|
|
||||||
// checks if an uri matches the service's pattern
|
// checks if an uri matches the service's pattern
|
||||||
func (svc *Service) matchPattern(uri string) bool {
|
func (svc *Service) matchPattern(uri string) bool {
|
||||||
uriparts := splitURL(uri)
|
uriparts := SplitURL(uri)
|
||||||
parts := splitURL(svc.Pattern)
|
parts := SplitURL(svc.Pattern)
|
||||||
|
|
||||||
// fail if size differ
|
// fail if size differ
|
||||||
if len(uriparts) != len(parts) {
|
if len(uriparts) != len(parts) {
|
|
@ -3,11 +3,39 @@ package config
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/config/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
|
// Server represents a full server configuration
|
||||||
|
type Server struct {
|
||||||
|
Types []datatype.T
|
||||||
|
Services []*Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service represents a service definition (from api.json)
|
||||||
|
type Service struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Pattern string `json:"path"`
|
||||||
|
Scope [][]string `json:"scope"`
|
||||||
|
Description string `json:"info"`
|
||||||
|
Input map[string]*Parameter `json:"in"`
|
||||||
|
// Download *bool `json:"download"`
|
||||||
|
// Output map[string]*Parameter `json:"out"`
|
||||||
|
|
||||||
|
// references to url parameters
|
||||||
|
// format: '/uri/{param}'
|
||||||
|
Captures []*BraceCapture
|
||||||
|
|
||||||
|
// references to Query parameters
|
||||||
|
// format: 'GET@paranName'
|
||||||
|
Query map[string]*Parameter
|
||||||
|
|
||||||
|
// references for form parameters (all but Captures and Query)
|
||||||
|
Form map[string]*Parameter
|
||||||
|
}
|
||||||
|
|
||||||
// Parameter represents a parameter definition (from api.json)
|
// Parameter represents a parameter definition (from api.json)
|
||||||
type Parameter struct {
|
type Parameter struct {
|
||||||
Description string `json:"info"`
|
Description string `json:"info"`
|
||||||
|
@ -20,19 +48,9 @@ type Parameter struct {
|
||||||
Validator datatype.Validator
|
Validator datatype.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service represents a service definition (from api.json)
|
// BraceCapture links to the related URI parameter
|
||||||
type Service struct {
|
type BraceCapture struct {
|
||||||
Method string `json:"method"`
|
Name string
|
||||||
Pattern string `json:"path"`
|
Index int
|
||||||
Scope [][]string `json:"scope"`
|
Ref *Parameter
|
||||||
Description string `json:"info"`
|
|
||||||
Input map[string]*Parameter `json:"in"`
|
|
||||||
// Download *bool `json:"download"`
|
|
||||||
// Output map[string]*Parameter `json:"out"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server represents a full server configuration
|
|
||||||
type Server struct {
|
|
||||||
types []datatype.DataType
|
|
||||||
services []*Service
|
|
||||||
}
|
}
|
|
@ -1,15 +1,21 @@
|
||||||
package multipart
|
package multipart
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/internal/cerr"
|
// Error allows you to create constant "const" error with type boxing.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err Error) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
// ErrMissingDataName is set when a multipart variable/file has no name="..."
|
||||||
const ErrMissingDataName = cerr.Error("data has no name")
|
const ErrMissingDataName = Error("data has no name")
|
||||||
|
|
||||||
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
// ErrDataNameConflict is set when a multipart variable/file name is already used
|
||||||
const ErrDataNameConflict = cerr.Error("data name conflict")
|
const ErrDataNameConflict = Error("data name conflict")
|
||||||
|
|
||||||
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
// ErrNoHeader is set when a multipart variable/file has no (valid) header
|
||||||
const ErrNoHeader = cerr.Error("data has no header")
|
const ErrNoHeader = Error("data has no header")
|
||||||
|
|
||||||
// Component represents a multipart variable/file
|
// Component represents a multipart variable/file
|
||||||
type Component struct {
|
type Component struct {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
// Error allows you to create constant "const" error with type boxing.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
// Error implements the error builtin interface.
|
||||||
|
func (err Error) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnknownType is returned when encountering an unknown type
|
||||||
|
const ErrUnknownType = Error("unknown type")
|
||||||
|
|
||||||
|
// ErrInvalidJSON is returned when json parse failed
|
||||||
|
const ErrInvalidJSON = Error("invalid json")
|
||||||
|
|
||||||
|
// ErrInvalidRootType is returned when json is a map
|
||||||
|
const ErrInvalidRootType = Error("invalid json root type")
|
||||||
|
|
||||||
|
// ErrInvalidParamName - parameter has an invalid
|
||||||
|
const ErrInvalidParamName = Error("invalid parameter name")
|
||||||
|
|
||||||
|
// ErrMissingRequiredParam - required param is missing
|
||||||
|
const ErrMissingRequiredParam = Error("missing required param")
|
||||||
|
|
||||||
|
// ErrInvalidType - parameter value does not satisfy its type
|
||||||
|
const ErrInvalidType = Error("invalid type")
|
||||||
|
|
||||||
|
// ErrMissingURIParameter - missing an URI parameter
|
||||||
|
const ErrMissingURIParameter = Error("missing URI parameter")
|
|
@ -4,19 +4,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrUnknownType is returned when encountering an unknown type
|
|
||||||
const ErrUnknownType = cerr.Error("unknown type")
|
|
||||||
|
|
||||||
// ErrInvalidJSON is returned when json parse failed
|
|
||||||
const ErrInvalidJSON = cerr.Error("invalid json")
|
|
||||||
|
|
||||||
// ErrInvalidRootType is returned when json is a map
|
|
||||||
const ErrInvalidRootType = cerr.Error("invalid json root type")
|
|
||||||
|
|
||||||
// Parameter represents an http request parameter
|
// Parameter represents an http request parameter
|
||||||
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
||||||
type Parameter struct {
|
type Parameter struct {
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
"git.xdrm.io/go/aicra/internal/multipart"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set represents all data that can be caught:
|
||||||
|
// - URI (from the URI)
|
||||||
|
// - GET (default url data)
|
||||||
|
// - POST (from json, form-data, url-encoded)
|
||||||
|
// - 'application/json' => key-value pair is parsed as json into the map
|
||||||
|
// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
||||||
|
// - 'multipart/form-data' => parse form-data format
|
||||||
|
type Set struct {
|
||||||
|
service *config.Service
|
||||||
|
|
||||||
|
// contains URL+GET+FORM data with prefixes:
|
||||||
|
// - FORM: no prefix
|
||||||
|
// - URL: '{uri_var}'
|
||||||
|
// - GET: 'GET@' followed by the key in GET
|
||||||
|
Data map[string]*Parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new empty store.
|
||||||
|
func New(service *config.Service) *Set {
|
||||||
|
return &Set{
|
||||||
|
service: service,
|
||||||
|
Data: make(map[string]*Parameter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractURI fills 'Set' with creating pointers inside 'Url'
|
||||||
|
func (i *Set) ExtractURI(req *http.Request) error {
|
||||||
|
uriparts := config.SplitURL(req.URL.RequestURI())
|
||||||
|
|
||||||
|
for _, capture := range i.service.Captures {
|
||||||
|
// out of range
|
||||||
|
if capture.Index > len(uriparts)-1 {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrMissingURIParameter)
|
||||||
|
}
|
||||||
|
value := uriparts[capture.Index]
|
||||||
|
|
||||||
|
// should not happen
|
||||||
|
if capture.Ref == nil {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := capture.Ref.Validator(value)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store cast value in 'Set'
|
||||||
|
i.Data[capture.Ref.Rename] = &Parameter{
|
||||||
|
Value: cast,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractQuery data from the url query parameters
|
||||||
|
func (i *Set) ExtractQuery(req *http.Request) error {
|
||||||
|
query := req.URL.Query()
|
||||||
|
|
||||||
|
for name, param := range i.service.Query {
|
||||||
|
value, exist := query[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := param.Validator(value)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value
|
||||||
|
i.Data[param.Rename] = &Parameter{
|
||||||
|
Value: cast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractForm data from request
|
||||||
|
//
|
||||||
|
// - parse 'form-data' if not supported for non-POST requests
|
||||||
|
// - parse 'x-www-form-urlencoded'
|
||||||
|
// - parse 'application/json'
|
||||||
|
func (i *Set) ExtractForm(req *http.Request) error {
|
||||||
|
|
||||||
|
// ignore GET method
|
||||||
|
if req.Method == http.MethodGet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
// parse json
|
||||||
|
if strings.HasPrefix(contentType, "application/json") {
|
||||||
|
return i.parseJSON(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse urlencoded
|
||||||
|
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||||
|
return i.parseUrlencoded(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse multipart
|
||||||
|
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||||
|
return i.parseMultipart(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to parse
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJSON parses JSON from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseJSON(req *http.Request) error {
|
||||||
|
|
||||||
|
parsed := make(map[string]interface{}, 0)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(req.Body)
|
||||||
|
if err := decoder.Decode(&parsed); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %w", err, ErrInvalidJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
value, exist := parsed[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
|
cast, valid := param.Validator(value)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value
|
||||||
|
i.Data[param.Rename] = &Parameter{
|
||||||
|
Value: cast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseUrlencoded(req *http.Request) error {
|
||||||
|
// use http.Request interface
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
value, exist := req.PostForm[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type
|
||||||
|
cast, valid := param.Validator(value)
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value
|
||||||
|
i.Data[param.Rename] = &Parameter{
|
||||||
|
Value: cast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMultipart parses multi-part from the request body inside 'Form'
|
||||||
|
// and 'Set'
|
||||||
|
func (i *Set) parseMultipart(req *http.Request) error {
|
||||||
|
|
||||||
|
// 1. create reader
|
||||||
|
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||||
|
mpr, err := multipart.NewReader(req.Body, boundary)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. parse multipart
|
||||||
|
if err = mpr.Parse(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", err, ErrInvalidMultipart)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, param := range i.service.Form {
|
||||||
|
component, exist := mpr.Data[name]
|
||||||
|
|
||||||
|
// fail on missing required
|
||||||
|
if !exist && !param.Optional {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail on invalid type
|
||||||
|
cast, valid := param.Validator(string(component.Data))
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("%s: %w", name, ErrInvalidType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value
|
||||||
|
i.Data[param.Rename] = &Parameter{
|
||||||
|
Value: cast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,784 @@
|
||||||
|
package reqdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.xdrm.io/go/aicra/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEmptyService() *config.Service {
|
||||||
|
return &config.Service{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceWithURI(capturingBraces ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
|
||||||
|
for _, capture := range capturingBraces {
|
||||||
|
if len(capture) == 0 {
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id := fmt.Sprintf("{%s}", capture)
|
||||||
|
service.Input[id] = &config.Parameter{
|
||||||
|
Rename: capture,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Captures = append(service.Captures, &config.BraceCapture{
|
||||||
|
Name: capture,
|
||||||
|
Index: index,
|
||||||
|
Ref: service.Input[id],
|
||||||
|
})
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
func getServiceWithQuery(params ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
Query: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range params {
|
||||||
|
id := fmt.Sprintf("GET@%s", name)
|
||||||
|
service.Input[id] = &config.Parameter{
|
||||||
|
Rename: name,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Query[name] = service.Input[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
func getServiceWithForm(params ...string) *config.Service {
|
||||||
|
service := &config.Service{
|
||||||
|
Input: make(map[string]*config.Parameter),
|
||||||
|
Form: make(map[string]*config.Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range params {
|
||||||
|
service.Input[name] = &config.Parameter{
|
||||||
|
Rename: name,
|
||||||
|
Validator: func(value interface{}) (interface{}, bool) { return value, true },
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Form[name] = service.Input[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreWithUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
URI string
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]string{},
|
||||||
|
"/non-captured/uri",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"missing"},
|
||||||
|
"/",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "missing"},
|
||||||
|
"/gotme",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "gotittoo"},
|
||||||
|
"/gotme/andme",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"gotit", "gotittoo"},
|
||||||
|
"/gotme/andme/ignored",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"first", "", "second"},
|
||||||
|
"/gotme/ignored/gotmetoo",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"first", "", "second"},
|
||||||
|
"/gotme/ignored",
|
||||||
|
ErrMissingURIParameter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("test.%d", i), func(t *testing.T) {
|
||||||
|
service := getServiceWithURI(test.ServiceParams...)
|
||||||
|
store := New(service)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil)
|
||||||
|
err := store.ExtractURI(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(store.Data) != len(service.Input) {
|
||||||
|
t.Errorf("store should contain %d elements, got %d", len(service.Input), len(store.Data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractQuery(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParam []string
|
||||||
|
Query string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues [][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ServiceParam: []string{},
|
||||||
|
Query: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"missing"},
|
||||||
|
Query: "",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Query: "a",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "missing"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "b"},
|
||||||
|
Query: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=",
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "b"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=&b=x",
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=b&c=d",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParam: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
Query: "a=b&c=d&a=x",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
|
||||||
|
store := New(getServiceWithQuery(test.ServiceParam...))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
||||||
|
err := store.ExtractQuery(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, pName := range test.ParamNames {
|
||||||
|
values := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(pName, func(t *testing.T) {
|
||||||
|
param, isset := store.Data[pName]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := param.Value.([]string)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("should return a []string (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cast) != len(values) {
|
||||||
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
|
if value != cast[vi] {
|
||||||
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
||||||
|
// http.Request.ParseForm() fails when:
|
||||||
|
// - http.Request.Method is one of [POST,PUT,PATCH]
|
||||||
|
// - http.Request.Form is not nil (created manually)
|
||||||
|
// - http.Request.PostForm is nil (deleted manually)
|
||||||
|
// - http.Request.Body is nil (deleted manually)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
// break everything
|
||||||
|
req.Body = nil
|
||||||
|
req.Form = make(url.Values)
|
||||||
|
req.PostForm = nil
|
||||||
|
|
||||||
|
// defer req.Body.Close()
|
||||||
|
store := New(nil)
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestExtractFormUrlEncoded(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
URLEncoded string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues [][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
URLEncoded: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"missing"},
|
||||||
|
URLEncoded: "",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
URLEncoded: "a",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "missing"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: ErrMissingRequiredParam,
|
||||||
|
ParamNames: nil,
|
||||||
|
ParamValues: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "b"},
|
||||||
|
URLEncoded: "a&b",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=",
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: [][]string{[]string{""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "b"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=&b=x",
|
||||||
|
ParamNames: []string{"a", "b"},
|
||||||
|
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=b&c=d",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Err: nil,
|
||||||
|
URLEncoded: "a=b&c=d&a=x",
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.URLEncoded)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no GET parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, key := range test.ParamNames {
|
||||||
|
values := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("param does not exist")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
cast, canCast := param.Value.([]string)
|
||||||
|
if !canCast {
|
||||||
|
t.Errorf("should return a []string (got '%v')", cast)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cast) != len(values) {
|
||||||
|
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for vi, value := range values {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
||||||
|
if value != cast[vi] {
|
||||||
|
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
Raw string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues []interface{}
|
||||||
|
}{
|
||||||
|
// no need to fully check json because it is parsed with the standard library
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: "",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: "{}",
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: `{ "a": "b" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": "b" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": "b", "ignored": "d" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
Raw: `{ "a": "b", "c": "d" }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: []interface{}{"b", "d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
Raw: `{ "a": null }`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{nil},
|
||||||
|
},
|
||||||
|
// json parse error
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
Raw: `{ "a": "b", }`,
|
||||||
|
Err: ErrInvalidJSON,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.Raw)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
defer req.Body.Close()
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, pName := range test.ParamNames {
|
||||||
|
key := pName
|
||||||
|
value := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType := reflect.TypeOf(value)
|
||||||
|
|
||||||
|
paramValue := param.Value
|
||||||
|
paramValueType := reflect.TypeOf(param.Value)
|
||||||
|
|
||||||
|
if valueType != paramValueType {
|
||||||
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramValue != value {
|
||||||
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipartParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ServiceParams []string
|
||||||
|
RawMultipart string
|
||||||
|
Err error
|
||||||
|
|
||||||
|
ParamNames []string
|
||||||
|
ParamValues []interface{}
|
||||||
|
}{
|
||||||
|
// no need to fully check json because it is parsed with the standard library
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: ``,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
`,
|
||||||
|
Err: ErrInvalidMultipart,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
--xxx--`,
|
||||||
|
Err: ErrInvalidMultipart,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx--`,
|
||||||
|
ParamNames: []string{},
|
||||||
|
ParamValues: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx--`,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a", "c"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="c"
|
||||||
|
|
||||||
|
d
|
||||||
|
--xxx--`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a", "c"},
|
||||||
|
ParamValues: []interface{}{"b", "d"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ServiceParams: []string{"a"},
|
||||||
|
RawMultipart: `--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
b
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="ignored"
|
||||||
|
|
||||||
|
x
|
||||||
|
--xxx--`,
|
||||||
|
Err: nil,
|
||||||
|
ParamNames: []string{"a"},
|
||||||
|
ParamValues: []interface{}{"b"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
||||||
|
body := strings.NewReader(test.RawMultipart)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
||||||
|
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
||||||
|
defer req.Body.Close()
|
||||||
|
store := New(getServiceWithForm(test.ServiceParams...))
|
||||||
|
|
||||||
|
err := store.ExtractForm(req)
|
||||||
|
if err != nil {
|
||||||
|
if test.Err != nil {
|
||||||
|
if !errors.Is(err, test.Err) {
|
||||||
|
t.Errorf("expected error <%s>, got <%s>", test.Err, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("unexpected error <%s>", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.ParamNames == nil || test.ParamValues == nil {
|
||||||
|
if len(store.Data) != 0 {
|
||||||
|
t.Errorf("expected no JSON parameters and got %d", len(store.Data))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no param to check
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test.ParamNames) != len(test.ParamValues) {
|
||||||
|
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for pi, key := range test.ParamNames {
|
||||||
|
value := test.ParamValues[pi]
|
||||||
|
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
|
||||||
|
param, isset := store.Data[key]
|
||||||
|
if !isset {
|
||||||
|
t.Errorf("store should contain element with key '%s'", key)
|
||||||
|
t.FailNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueType := reflect.TypeOf(value)
|
||||||
|
|
||||||
|
paramValue := param.Value
|
||||||
|
paramValueType := reflect.TypeOf(param.Value)
|
||||||
|
|
||||||
|
if valueType != paramValueType {
|
||||||
|
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if paramValue != value {
|
||||||
|
t.Errorf("should return %v (got '%v')", value, paramValue)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,301 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/internal/multipart"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store represents all data that can be caught:
|
|
||||||
// - URI (guessed from the URI by removing the service path)
|
|
||||||
// - GET (default url data)
|
|
||||||
// - POST (from json, form-data, url-encoded)
|
|
||||||
type Store struct {
|
|
||||||
|
|
||||||
// ordered values from the URI
|
|
||||||
// catches all after the service path
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
URI []*Parameter
|
|
||||||
|
|
||||||
// uri parameters following the QUERY format
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
Get map[string]*Parameter
|
|
||||||
|
|
||||||
// form data depending on the Content-Type:
|
|
||||||
// 'application/json' => key-value pair is parsed as json into the map
|
|
||||||
// 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters
|
|
||||||
// 'multipart/form-data' => parse form-data format
|
|
||||||
//
|
|
||||||
// points to Store.Data
|
|
||||||
Form map[string]*Parameter
|
|
||||||
|
|
||||||
// contains URL+GET+FORM data with prefixes:
|
|
||||||
// - FORM: no prefix
|
|
||||||
// - URL: 'URL#' followed by the index in Uri
|
|
||||||
// - GET: 'GET@' followed by the key in GET
|
|
||||||
Set map[string]*Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new store from an http request.
|
|
||||||
// URI params is required because it only takes into account after service path
|
|
||||||
// we do not know in this scope.
|
|
||||||
func New(uriParams []string, req *http.Request) *Store {
|
|
||||||
ds := &Store{
|
|
||||||
URI: make([]*Parameter, 0),
|
|
||||||
Get: make(map[string]*Parameter),
|
|
||||||
Form: make(map[string]*Parameter),
|
|
||||||
Set: make(map[string]*Parameter),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. set URI parameters
|
|
||||||
ds.setURIParams(uriParams)
|
|
||||||
|
|
||||||
// ignore nil requests
|
|
||||||
if req == nil {
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. GET (query) data
|
|
||||||
ds.readQuery(req)
|
|
||||||
|
|
||||||
// 3. We are done if GET method
|
|
||||||
if req.Method == http.MethodGet {
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. POST (body) data
|
|
||||||
ds.readForm(req)
|
|
||||||
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
// setURIParameters fills 'Set' with creating pointers inside 'Url'
|
|
||||||
func (i *Store) setURIParams(orderedUParams []string) {
|
|
||||||
|
|
||||||
for index, value := range orderedUParams {
|
|
||||||
|
|
||||||
// create set index
|
|
||||||
setindex := fmt.Sprintf("URL#%d", index)
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[setindex] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Url'
|
|
||||||
i.URI = append(i.URI, i.Set[setindex])
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// readQuery stores data from the QUERY (in url parameters)
|
|
||||||
func (i *Store) readQuery(req *http.Request) {
|
|
||||||
|
|
||||||
for name, value := range req.URL.Query() {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("get.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// create set index
|
|
||||||
setindex := fmt.Sprintf("GET@%s", name)
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[setindex] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Get'
|
|
||||||
i.Get[name] = i.Set[setindex]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// readForm stores FORM data
|
|
||||||
//
|
|
||||||
// - parse 'form-data' if not supported (not POST requests)
|
|
||||||
// - parse 'x-www-form-urlencoded'
|
|
||||||
// - parse 'application/json'
|
|
||||||
func (i *Store) readForm(req *http.Request) {
|
|
||||||
|
|
||||||
contentType := req.Header.Get("Content-Type")
|
|
||||||
|
|
||||||
// parse json
|
|
||||||
if strings.HasPrefix(contentType, "application/json") {
|
|
||||||
i.parseJSON(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse urlencoded
|
|
||||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
|
||||||
i.parseUrlencoded(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse multipart
|
|
||||||
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
|
||||||
i.parseMultipart(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if unknown type store nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseJSON parses JSON from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseJSON(req *http.Request) {
|
|
||||||
|
|
||||||
parsed := make(map[string]interface{}, 0)
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(req.Body)
|
|
||||||
|
|
||||||
// if parse error: do nothing
|
|
||||||
if err := decoder.Decode(&parsed); err != nil {
|
|
||||||
log.Printf("json.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// else store values 'parsed' values
|
|
||||||
for name, value := range parsed {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: true,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseUrlencoded parses urlencoded from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseUrlencoded(req *http.Request) {
|
|
||||||
|
|
||||||
// use http.Request interface
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
log.Printf("urlencoded.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range req.PostForm {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseMultipart parses multi-part from the request body inside 'Form'
|
|
||||||
// and 'Set'
|
|
||||||
func (i *Store) parseMultipart(req *http.Request) {
|
|
||||||
|
|
||||||
/* (1) Create reader */
|
|
||||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
|
||||||
mpr, err := multipart.NewReader(req.Body, boundary)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2) Parse multipart */
|
|
||||||
if err = mpr.Parse(); err != nil {
|
|
||||||
log.Printf("multipart.parse() %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (3) Store data into 'Form' and 'Set */
|
|
||||||
for name, data := range mpr.Data {
|
|
||||||
|
|
||||||
// prevent invalid names
|
|
||||||
if !isNameValid(name) {
|
|
||||||
log.Printf("invalid variable name: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent injections
|
|
||||||
if hasNameInjection(name) {
|
|
||||||
log.Printf("post.injection: '%s'\n", name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// store value in 'Set'
|
|
||||||
i.Set[name] = &Parameter{
|
|
||||||
Parsed: false,
|
|
||||||
File: len(data.GetHeader("filename")) > 0,
|
|
||||||
Value: string(data.Data),
|
|
||||||
}
|
|
||||||
|
|
||||||
// create link in 'Form'
|
|
||||||
i.Form[name] = i.Set[name]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasNameInjection returns whether there is
|
|
||||||
// a parameter name injection:
|
|
||||||
// - inferred GET parameters
|
|
||||||
// - inferred URL parameters
|
|
||||||
func hasNameInjection(pName string) bool {
|
|
||||||
return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNameValid returns whether a parameter name (without the GET@ or URL# prefix) is valid
|
|
||||||
// if fails if the name begins/ends with underscores
|
|
||||||
func isNameValid(pName string) bool {
|
|
||||||
return strings.Trim(pName, "_") == pName
|
|
||||||
}
|
|
|
@ -1,804 +0,0 @@
|
||||||
package reqdata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEmptyStore(t *testing.T) {
|
|
||||||
store := New(nil, nil)
|
|
||||||
|
|
||||||
if store.URI == nil {
|
|
||||||
t.Errorf("store 'URI' list should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if len(store.URI) != 0 {
|
|
||||||
t.Errorf("store 'URI' list should be empty")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if store.Get == nil {
|
|
||||||
t.Errorf("store 'Get' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if store.Form == nil {
|
|
||||||
t.Errorf("store 'Form' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if store.Set == nil {
|
|
||||||
t.Errorf("store 'Set' map should be initialized")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreWithUri(t *testing.T) {
|
|
||||||
urilist := []string{"abc", "def"}
|
|
||||||
store := New(urilist, nil)
|
|
||||||
|
|
||||||
if len(store.URI) != len(urilist) {
|
|
||||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.URI))
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if len(store.Set) != len(urilist) {
|
|
||||||
t.Errorf("store 'Set' should contain %d elements (got %d)", len(urilist), len(store.Set))
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, value := range urilist {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("URL#%d='%s'", i, value), func(t *testing.T) {
|
|
||||||
key := fmt.Sprintf("URL#%d", i)
|
|
||||||
element, isset := store.Set[key]
|
|
||||||
|
|
||||||
if !isset {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if element.Value != value {
|
|
||||||
t.Errorf("store[%s] should return '%s' (got '%s')", key, value, element.Value)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreWithGet(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Query string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues [][]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Query: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: [][]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a&b",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=&b=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&c=d",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&c=d&a=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&_invalid=x",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&invalid_=x",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Query: "a=b&GET@injection=x",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{ // not really useful as all after '#' should be ignored by http clients
|
|
||||||
Query: "a=b&URL#injection=x",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil)
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no GET parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := fmt.Sprintf("GET@%s", pName)
|
|
||||||
values := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := param.Value.([]string)
|
|
||||||
|
|
||||||
if !canCast {
|
|
||||||
t.Errorf("should return a []string (got '%v')", cast)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
|
||||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for vi, value := range values {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
|
||||||
if value != cast[vi] {
|
|
||||||
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestStoreWithUrlEncodedFormParseError(t *testing.T) {
|
|
||||||
// http.Request.ParseForm() fails when:
|
|
||||||
// - http.Request.Method is one of [POST,PUT,PATCH]
|
|
||||||
// - http.Request.Form is not nil (created manually)
|
|
||||||
// - http.Request.PostForm is nil (deleted manually)
|
|
||||||
// - http.Request.Body is nil (deleted manually)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com/", nil)
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
|
|
||||||
// break everything
|
|
||||||
req.Body = nil
|
|
||||||
req.Form = make(url.Values)
|
|
||||||
req.PostForm = nil
|
|
||||||
|
|
||||||
// defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
if len(store.Form) > 0 {
|
|
||||||
t.Errorf("expected malformed urlencoded to have failed being parsed (got %d elements)", len(store.Form))
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestStoreWithUrlEncodedForm(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
URLEncoded string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues [][]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
URLEncoded: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: [][]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a&b",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: [][]string{[]string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=&b=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "b"},
|
|
||||||
ParamValues: [][]string{[]string{""}, []string{"x"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&c=d",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&c=d&a=x",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: [][]string{[]string{"b", "x"}, []string{"d"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&_invalid=x",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&invalid_=x",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&GET@injection=x",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
URLEncoded: "a=b&URL#injection=x",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: [][]string{[]string{"b"}, []string{""}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.URLEncoded)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no FORM parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
values := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
cast, canCast := param.Value.([]string)
|
|
||||||
|
|
||||||
if !canCast {
|
|
||||||
t.Errorf("should return a []string (got '%v')", cast)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cast) != len(values) {
|
|
||||||
t.Errorf("should return %d string(s) (got '%d')", len(values), len(cast))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for vi, value := range values {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("value.%d", vi), func(t *testing.T) {
|
|
||||||
if value != cast[vi] {
|
|
||||||
t.Errorf("should return '%s' (got '%s')", value, cast[vi])
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJsonParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
RawJson string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues []interface{}
|
|
||||||
}{
|
|
||||||
// no need to fully check json because it is parsed with the standard library
|
|
||||||
{
|
|
||||||
RawJson: "",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{}",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\" }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: []interface{}{"b"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"c\": \"d\" }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: []interface{}{"b", "d"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"_invalid\": \"x\" }",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"_invalid"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"_invalid\": \"x\" }",
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"invalid_\": \"x\" }",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"invalid_"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"invalid_\": \"x\" }",
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"GET@injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"GET@injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"GET@injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawJson: "{ \"URL#injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"URL#injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", \"URL#injection\": \"x\" }",
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
// json parse error
|
|
||||||
{
|
|
||||||
RawJson: "{ \"a\": \"b\", }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.RawJson)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
value := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
valueType := reflect.TypeOf(value)
|
|
||||||
|
|
||||||
paramValue := param.Value
|
|
||||||
paramValueType := reflect.TypeOf(param.Value)
|
|
||||||
|
|
||||||
if valueType != paramValueType {
|
|
||||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if paramValue != value {
|
|
||||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipartParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
RawMultipart string
|
|
||||||
|
|
||||||
InvalidNames []string
|
|
||||||
ParamNames []string
|
|
||||||
ParamValues []interface{}
|
|
||||||
}{
|
|
||||||
// no need to fully check json because it is parsed with the standard library
|
|
||||||
{
|
|
||||||
RawMultipart: ``,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a"},
|
|
||||||
ParamValues: []interface{}{"b"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="c"
|
|
||||||
|
|
||||||
d
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{"a", "c"},
|
|
||||||
ParamValues: []interface{}{"b", "d"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="_invalid"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"_invalid"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="_invalid"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"_invalid"},
|
|
||||||
ParamNames: []string{"a", "_invalid"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="invalid_"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"invalid_"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="invalid_"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"invalid_"},
|
|
||||||
ParamNames: []string{"a", "invalid_"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="GET@injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"GET@injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="GET@injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"GET@injection"},
|
|
||||||
ParamNames: []string{"a", "GET@injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="URL#injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"URL#injection"},
|
|
||||||
ParamValues: []interface{}{nil},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RawMultipart: `--xxx
|
|
||||||
Content-Disposition: form-data; name="a"
|
|
||||||
|
|
||||||
b
|
|
||||||
--xxx
|
|
||||||
Content-Disposition: form-data; name="URL#injection"
|
|
||||||
|
|
||||||
x
|
|
||||||
--xxx--`,
|
|
||||||
InvalidNames: []string{"URL#injection"},
|
|
||||||
ParamNames: []string{"a", "URL#injection"},
|
|
||||||
ParamValues: []interface{}{"b", nil},
|
|
||||||
},
|
|
||||||
// json parse error
|
|
||||||
{
|
|
||||||
RawMultipart: "{ \"a\": \"b\", }",
|
|
||||||
InvalidNames: []string{},
|
|
||||||
ParamNames: []string{},
|
|
||||||
ParamValues: []interface{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("request.%d", i), func(t *testing.T) {
|
|
||||||
body := strings.NewReader(test.RawMultipart)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "http://host.com", body)
|
|
||||||
req.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
|
||||||
defer req.Body.Close()
|
|
||||||
store := New(nil, req)
|
|
||||||
|
|
||||||
if test.ParamNames == nil || test.ParamValues == nil {
|
|
||||||
if len(store.Set) != 0 {
|
|
||||||
t.Errorf("expected no JSON parameters and got %d", len(store.Get))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no param to check
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(test.ParamNames) != len(test.ParamValues) {
|
|
||||||
t.Errorf("invalid test: names and values differ in size (%d vs %d)", len(test.ParamNames), len(test.ParamValues))
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
for pi, pName := range test.ParamNames {
|
|
||||||
key := pName
|
|
||||||
value := test.ParamValues[pi]
|
|
||||||
|
|
||||||
isNameValid := true
|
|
||||||
for _, invalid := range test.InvalidNames {
|
|
||||||
if pName == invalid {
|
|
||||||
isNameValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(key, func(t *testing.T) {
|
|
||||||
|
|
||||||
param, isset := store.Set[key]
|
|
||||||
if !isset {
|
|
||||||
if isNameValid {
|
|
||||||
t.Errorf("store should contain element with key '%s'", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if should be invalid
|
|
||||||
if isset && !isNameValid {
|
|
||||||
t.Errorf("store should NOT contain element with key '%s' (invalid name)", key)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
valueType := reflect.TypeOf(value)
|
|
||||||
|
|
||||||
paramValue := param.Value
|
|
||||||
paramValueType := reflect.TypeOf(param.Value)
|
|
||||||
|
|
||||||
if valueType != paramValueType {
|
|
||||||
t.Errorf("should be of type %v (got '%v')", valueType, paramValueType)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if paramValue != value {
|
|
||||||
t.Errorf("should return %v (got '%v')", value, paramValue)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package typecheck
|
|
||||||
|
|
||||||
// Set of type checkers
|
|
||||||
type Set struct {
|
|
||||||
types []Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new set of type checkers
|
|
||||||
func New() *Set {
|
|
||||||
return &Set{types: make([]Type, 0)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a new type checker
|
|
||||||
func (s *Set) Add(typeChecker Type) {
|
|
||||||
s.types = append(s.types, typeChecker)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run finds a type checker from the registry matching the type `typeName`
|
|
||||||
// and uses this checker to check the `value`. If no type checker matches
|
|
||||||
// the `type`, error is returned by default.
|
|
||||||
func (s *Set) Run(typeName string, value interface{}) error {
|
|
||||||
|
|
||||||
// find matching type (take first)
|
|
||||||
for _, typeChecker := range s.types {
|
|
||||||
if typeChecker == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// found
|
|
||||||
checkerFunc := typeChecker.Checker(typeName)
|
|
||||||
if checkerFunc == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check value
|
|
||||||
if checkerFunc(value) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ErrDoesNotMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrNoMatchingType
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package typecheck
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// ErrNoMatchingType when no available type checker matches the type
|
|
||||||
var ErrNoMatchingType = errors.New("no matching type")
|
|
||||||
|
|
||||||
// ErrDoesNotMatch when the value is invalid
|
|
||||||
var ErrDoesNotMatch = errors.New("does not match")
|
|
||||||
|
|
||||||
// CheckerFunc returns whether a given value fulfills a type
|
|
||||||
type CheckerFunc func(interface{}) bool
|
|
||||||
|
|
||||||
// Type represents a type checker
|
|
||||||
type Type interface {
|
|
||||||
// given a type name, returns the checker function or NIL if the type is not handled here
|
|
||||||
Checker(string) CheckerFunc
|
|
||||||
}
|
|
Loading…
Reference in New Issue