Compare commits
No commits in common. "3606f9984d8499442b5c7b37240fb7fc8163ce4d" and "32aff3e07fa597fb2e50efdf94749cf407f9d5ca" have entirely different histories.
3606f9984d
...
32aff3e07f
|
@ -2,21 +2,15 @@ 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 = Error("request parameter not found")
|
const ErrReqParamNotFound = cerr.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 = Error("request parameter does not fulfills type")
|
const ErrReqParamNotType = cerr.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{}
|
||||||
|
|
|
@ -8,12 +8,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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
|
||||||
|
@ -45,35 +43,35 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternBracePosition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternBracePosition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
||||||
ErrUndefinedBraceCapture,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternBracePosition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternBracePosition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
||||||
ErrUndefinedBraceCapture,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternOpeningBrace,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
||||||
ErrInvalidPatternBraceCapture,
|
ErrInvalidPatternClosingBrace,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +99,6 @@ 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
|
||||||
|
@ -149,7 +146,6 @@ 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 {
|
||||||
|
@ -171,7 +167,6 @@ 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
|
||||||
|
@ -222,7 +217,6 @@ 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",
|
||||||
|
@ -239,12 +233,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()
|
||||||
|
@ -253,7 +247,6 @@ 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",
|
||||||
|
@ -273,11 +266,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 {
|
||||||
|
@ -295,7 +288,6 @@ 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
|
||||||
|
@ -311,7 +303,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
ErrIllegalParamName,
|
||||||
},
|
},
|
||||||
{ // invalid param name suffix
|
{ // invalid param name suffix
|
||||||
`[
|
`[
|
||||||
|
@ -324,7 +316,7 @@ func TestParseParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`,
|
]`,
|
||||||
ErrMissingParamDesc,
|
ErrIllegalParamName,
|
||||||
},
|
},
|
||||||
|
|
||||||
{ // missing param description
|
{ // missing param description
|
||||||
|
@ -481,57 +473,6 @@ 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 {
|
||||||
|
@ -559,8 +500,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
|
||||||
|
@ -642,7 +583,7 @@ func TestMatchSimple(t *testing.T) {
|
||||||
"path": "/a/{valid}",
|
"path": "/a/{valid}",
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"in": {
|
"in": {
|
||||||
"{valid}": {
|
"{id}": {
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
}
|
}
|
||||||
|
@ -657,7 +598,7 @@ func TestMatchSimple(t *testing.T) {
|
||||||
"path": "/a/{valid}",
|
"path": "/a/{valid}",
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"in": {
|
"in": {
|
||||||
"{valid}": {
|
"{id}": {
|
||||||
"info": "info",
|
"info": "info",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
}
|
}
|
||||||
|
@ -678,14 +619,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()
|
|
@ -1,6 +1,6 @@
|
||||||
package builtin
|
package builtin
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/datatype"
|
import "git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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/datatype"
|
import "git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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/datatype"
|
"git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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/datatype"
|
"git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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/datatype"
|
"git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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/datatype"
|
"git.xdrm.io/go/aicra/config/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/datatype/builtin"
|
"git.xdrm.io/go/aicra/config/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)
|
||||||
|
|
||||||
// T builds a T from the type definition (from the
|
// DataType builds a DataType 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 T
|
// definition does not match this DataType
|
||||||
type T interface {
|
type DataType interface {
|
||||||
Build(typeDefinition string) Validator
|
Build(typeDefinition string) Validator
|
||||||
}
|
}
|
|
@ -23,21 +23,18 @@ 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")
|
||||||
|
|
||||||
// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid
|
// ErrInvalidPatternBracePosition - a service pattern opening/closing brace is not directly between '/'
|
||||||
const ErrInvalidPatternBraceCapture = Error("invalid uri capturing braces")
|
const ErrInvalidPatternBracePosition = Error("capturing braces must be alone between slashes")
|
||||||
|
|
||||||
// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern
|
// ErrInvalidPatternOpeningBrace - a service pattern opening brace is invalid
|
||||||
const ErrUnspecifiedBraceCapture = Error("capturing brace missing in the path")
|
const ErrInvalidPatternOpeningBrace = Error("opening brace already open")
|
||||||
|
|
||||||
// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters
|
// ErrInvalidPatternClosingBrace - a service pattern closing brace is invalid
|
||||||
const ErrUndefinedBraceCapture = Error("capturing brace missing input definition")
|
const ErrInvalidPatternClosingBrace = Error("closing brace already closed")
|
||||||
|
|
||||||
// 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")
|
||||||
|
|
||||||
|
@ -45,7 +42,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("illegal parameter name")
|
const ErrIllegalParamName = Error("parameter name must not begin/end with '_'")
|
||||||
|
|
||||||
// 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"
|
||||||
|
|
||||||
// SplitURL without empty sets
|
// splits an URL 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/datatype"
|
import "git.xdrm.io/go/aicra/config/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.T) bool {
|
func (param *Parameter) assignDataType(types []datatype.DataType) 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 {
|
|
@ -3,15 +3,11 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/config/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
|
||||||
|
@ -25,7 +21,7 @@ func (svc *Service) Match(req *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and extract input
|
// check and extract input
|
||||||
// todo: check if input match and extract models
|
// todo: check if input match
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -54,40 +50,40 @@ func (svc *Service) checkPattern() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each slash-separated chunk
|
// check capturing braces
|
||||||
parts := SplitURL(svc.Pattern)
|
depth := 0
|
||||||
for i, part := range parts {
|
for c, l := 1, length; c < l; c++ {
|
||||||
if len(part) < 1 {
|
char := svc.Pattern[c]
|
||||||
return ErrInvalidPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
// if brace capture
|
if char == '{' {
|
||||||
if matches := braceRegex.FindAllStringSubmatch(part, -1); len(matches) > 0 && len(matches[0]) > 1 {
|
// opening brace when already opened
|
||||||
braceName := matches[0][1]
|
if depth != 0 {
|
||||||
|
return ErrInvalidPatternOpeningBrace
|
||||||
// append
|
|
||||||
if svc.Captures == nil {
|
|
||||||
svc.Captures = make([]*BraceCapture, 0)
|
|
||||||
}
|
}
|
||||||
svc.Captures = append(svc.Captures, &BraceCapture{
|
|
||||||
Index: i,
|
|
||||||
Name: braceName,
|
|
||||||
Ref: nil,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail on invalid format
|
// not directly preceded by a slash
|
||||||
if strings.ContainsAny(part, "{}") {
|
if svc.Pattern[c-1] != '/' {
|
||||||
return ErrInvalidPatternBraceCapture
|
return ErrInvalidPatternBracePosition
|
||||||
|
}
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
if char == '}' {
|
||||||
|
// closing brace when already closed
|
||||||
|
if depth != 1 {
|
||||||
|
return ErrInvalidPatternClosingBrace
|
||||||
|
}
|
||||||
|
// 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.T) error {
|
func (svc *Service) checkAndFormatInput(types []datatype.DataType) error {
|
||||||
|
|
||||||
// ignore no parameter
|
// ignore no parameter
|
||||||
if svc.Input == nil || len(svc.Input) < 1 {
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
|
@ -97,45 +93,12 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) 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
|
||||||
|
@ -146,11 +109,6 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) 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)
|
||||||
}
|
}
|
||||||
|
@ -178,8 +136,8 @@ func (svc *Service) checkAndFormatInput(types []datatype.T) 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) {
|
|
@ -7,23 +7,23 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/config/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.T) (*Server, error) {
|
func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Types: make([]datatype.T, 0),
|
types: make([]datatype.DataType, 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,34 +40,23 @@ func Parse(r io.Reader, dtypes ...datatype.T) (*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) {
|
||||||
|
@ -131,9 +120,20 @@ 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,18 +154,11 @@ 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,39 +3,11 @@ package config
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/config/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"`
|
||||||
|
@ -48,9 +20,19 @@ type Parameter struct {
|
||||||
Validator datatype.Validator
|
Validator datatype.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
// BraceCapture links to the related URI parameter
|
// Service represents a service definition (from api.json)
|
||||||
type BraceCapture struct {
|
type Service struct {
|
||||||
Name string
|
Method string `json:"method"`
|
||||||
Index int
|
Pattern string `json:"path"`
|
||||||
Ref *Parameter
|
Scope [][]string `json:"scope"`
|
||||||
|
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
|
||||||
}
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,15 @@
|
||||||
package multipart
|
package multipart
|
||||||
|
|
||||||
// Error allows you to create constant "const" error with type boxing.
|
import "git.xdrm.io/go/aicra/internal/cerr"
|
||||||
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 = Error("data has no name")
|
const ErrMissingDataName = cerr.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 = Error("data name conflict")
|
const ErrDataNameConflict = cerr.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 = Error("data has no header")
|
const ErrNoHeader = cerr.Error("data has no header")
|
||||||
|
|
||||||
// Component represents a multipart variable/file
|
// Component represents a multipart variable/file
|
||||||
type Component struct {
|
type Component struct {
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
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,8 +4,19 @@ 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 {
|
||||||
|
|
|
@ -1,261 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,784 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,301 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,804 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
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