bcupdate: make config flat, rewrite, simplify, test
This commit is contained in:
parent
acdba4121b
commit
a6f5083f0d
|
@ -0,0 +1,632 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLegalServiceName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Error error
|
||||||
|
}{
|
||||||
|
// empty
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "" } ]`,
|
||||||
|
ErrInvalidPattern,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`,
|
||||||
|
ErrInvalidPattern,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`,
|
||||||
|
ErrInvalidPattern,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/" } ]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/valid-name" } ]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/valid/nested/name" } ]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`,
|
||||||
|
ErrInvalidPatternBracePosition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`,
|
||||||
|
ErrInvalidPatternBracePosition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`,
|
||||||
|
ErrInvalidPatternBracePosition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`,
|
||||||
|
ErrInvalidPatternBracePosition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`,
|
||||||
|
ErrInvalidPatternOpeningBrace,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`,
|
||||||
|
ErrInvalidPatternClosingBrace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err == nil && test.Error != nil {
|
||||||
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
if err != nil && test.Error == nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && test.Error != nil {
|
||||||
|
if !errors.Is(err, test.Error) {
|
||||||
|
t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestAvailableMethods(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
ValidMethod bool
|
||||||
|
}{
|
||||||
|
{ // missing description
|
||||||
|
`[ { "method": "GET", "path": "/", "info": "valid-description" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // missing description
|
||||||
|
`[ { "method": "POST", "path": "/", "info": "valid-description" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // empty description
|
||||||
|
`[ { "method": "PUT", "path": "/", "info": "valid-description" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // empty trimmed description
|
||||||
|
`[ { "method": "DELETE", "path": "/", "info": "valid-description" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`[ { "method": "get", "path": "/", "info": "valid-description" }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`[ { "method": "UNknOwN", "path": "/", "info": "valid-description" }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if test.ValidMethod && err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) {
|
||||||
|
t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestParseEmpty(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`[]`)
|
||||||
|
_, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestParseJsonError(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"GET": {
|
||||||
|
"info": "info
|
||||||
|
},
|
||||||
|
}`) // trailing ',' is invalid JSON
|
||||||
|
_, err := Parse(reader)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
ValidDescription bool
|
||||||
|
}{
|
||||||
|
{ // missing description
|
||||||
|
`[ { "method": "GET", "path": "/" }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // missing description
|
||||||
|
`[ { "method": "GET", "path": "/subservice" }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // empty description
|
||||||
|
`[ { "method": "GET", "path": "/", "info": "" }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // empty trimmed description
|
||||||
|
`[ { "method": "GET", "path": "/", "info": " " }]`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`[ { "method": "GET", "path": "/", "info": "a" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{ // valid description
|
||||||
|
`[ { "method": "GET", "path": "/", "info": "some description" }]`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if test.ValidDescription && err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) {
|
||||||
|
t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "valid-description",
|
||||||
|
"in": {
|
||||||
|
"original": { "info": "valid-desc", "type": "valid-type", "name": "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`)
|
||||||
|
srv, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(srv) < 1 {
|
||||||
|
t.Errorf("expected a service")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, param := range (srv)[0].Input {
|
||||||
|
if param.Rename != "original" {
|
||||||
|
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestOptionalParam(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "valid-description",
|
||||||
|
"in": {
|
||||||
|
"optional": { "info": "valid-desc", "type": "?optional-type" },
|
||||||
|
"required": { "info": "valid-desc", "type": "required-type" },
|
||||||
|
"required2": { "info": "valid-desc", "type": "a" },
|
||||||
|
"optional2": { "info": "valid-desc", "type": "?a" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`)
|
||||||
|
srv, err := Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(srv) < 1 {
|
||||||
|
t.Errorf("expected a service")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
for pName, param := range (srv)[0].Input {
|
||||||
|
|
||||||
|
if pName == "optional" || pName == "optional2" {
|
||||||
|
if !param.Optional {
|
||||||
|
t.Errorf("expected parameter '%s' to be optional", pName)
|
||||||
|
t.Failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pName == "required" || pName == "required2" {
|
||||||
|
if param.Optional {
|
||||||
|
t.Errorf("expected parameter '%s' to be required", pName)
|
||||||
|
t.Failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestParseParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Raw string
|
||||||
|
Error error
|
||||||
|
}{
|
||||||
|
{ // invalid param name prefix
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"_param1": { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrIllegalParamName,
|
||||||
|
},
|
||||||
|
{ // invalid param name suffix
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1_": { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrIllegalParamName,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // missing param description
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrMissingParamDesc,
|
||||||
|
},
|
||||||
|
{ // empty param description
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrMissingParamDesc,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // missing param type
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrMissingParamType,
|
||||||
|
},
|
||||||
|
{ // empty param type
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
ErrMissingParamType,
|
||||||
|
},
|
||||||
|
{ // invalid type (optional mark only)
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "?" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
|
||||||
|
ErrMissingParamType,
|
||||||
|
},
|
||||||
|
{ // valid description + valid type
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "a" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{ // valid description + valid OPTIONAL type
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "?valid" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // name conflict with rename
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "param1" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrParamNameConflict,
|
||||||
|
},
|
||||||
|
{ // rename conflict with name
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "param2" },
|
||||||
|
"param2": { "info": "valid", "type": "valid" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrParamNameConflict,
|
||||||
|
},
|
||||||
|
{ // rename conflict with rename
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "conflict" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "conflict" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
// 2 possible errors as map order is not deterministic
|
||||||
|
ErrParamNameConflict,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ // both renamed with no conflict
|
||||||
|
`[
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/",
|
||||||
|
"info": "info",
|
||||||
|
"in": {
|
||||||
|
"param1": { "info": "valid", "type": "valid", "name": "freename" },
|
||||||
|
"param2": { "info": "valid", "type": "valid", "name": "freename2" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
_, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
if err == nil && test.Error != nil {
|
||||||
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
if err != nil && test.Error == nil {
|
||||||
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && test.Error != nil {
|
||||||
|
if !errors.Is(err, test.Error) {
|
||||||
|
t.Errorf("expected the error <%s> got <%s>", test.Error, err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: rewrite with new api format
|
||||||
|
// func TestMatchSimple(t *testing.T) {
|
||||||
|
// tests := []struct {
|
||||||
|
// Raw string
|
||||||
|
// Path []string
|
||||||
|
// BrowseDepth int
|
||||||
|
// ValidDepth bool
|
||||||
|
// }{
|
||||||
|
// { // false positive -1
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"parent", "subdir"},
|
||||||
|
// 1,
|
||||||
|
// false,
|
||||||
|
// },
|
||||||
|
// { // false positive +1
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"parent", "subdir"},
|
||||||
|
// 3,
|
||||||
|
// false,
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"parent", "subdir"},
|
||||||
|
// 2,
|
||||||
|
// true,
|
||||||
|
// },
|
||||||
|
// { // unknown path
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"x", "y"},
|
||||||
|
// 2,
|
||||||
|
// false,
|
||||||
|
// },
|
||||||
|
// { // unknown path
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"parent", "y"},
|
||||||
|
// 1,
|
||||||
|
// true,
|
||||||
|
// },
|
||||||
|
// { // Warning: this case is important to understand the precedence of service paths over
|
||||||
|
// // the value of some variables. Here if we send a string parameter in the GET method that
|
||||||
|
// // unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead
|
||||||
|
// // of the service /parent with its parameter set to the value 'subdir'.
|
||||||
|
// `{
|
||||||
|
// "/" : {
|
||||||
|
// "parent": {
|
||||||
|
// "/": {
|
||||||
|
// "subdir": {}
|
||||||
|
// },
|
||||||
|
// "GET": {
|
||||||
|
// "info": "valid-desc",
|
||||||
|
// "in": {
|
||||||
|
// "some-value": {
|
||||||
|
// "info": "valid-desc",
|
||||||
|
// "type": "valid-type"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }`,
|
||||||
|
// []string{"parent", "subdir"},
|
||||||
|
// 2,
|
||||||
|
// true,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for i, test := range tests {
|
||||||
|
|
||||||
|
// t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
|
// srv, err := Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// t.Errorf("unexpected error: '%s'", err)
|
||||||
|
// t.FailNow()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// _, depth := srv.Match(test.Path)
|
||||||
|
// if test.ValidDepth {
|
||||||
|
// if depth != test.BrowseDepth {
|
||||||
|
// t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth)
|
||||||
|
// t.FailNow()
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if depth == test.BrowseDepth {
|
||||||
|
// t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth)
|
||||||
|
// t.FailNow()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
|
@ -0,0 +1,48 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrRead - a problem ocurred when trying to read the configuration file
|
||||||
|
const ErrRead = Error("cannot read config")
|
||||||
|
|
||||||
|
// ErrUnknownMethod - invalid http method
|
||||||
|
const ErrUnknownMethod = Error("unknown HTTP method")
|
||||||
|
|
||||||
|
// ErrFormat - a invalid format has been detected
|
||||||
|
const ErrFormat = Error("invalid config format")
|
||||||
|
|
||||||
|
// ErrPatternCollision - there is a collision between 2 services' patterns (same method)
|
||||||
|
const ErrPatternCollision = Error("invalid config format")
|
||||||
|
|
||||||
|
// ErrInvalidPattern - a service pattern is malformed
|
||||||
|
const ErrInvalidPattern = Error("must begin with a '/' and not end with")
|
||||||
|
|
||||||
|
// ErrInvalidPatternBracePosition - a service pattern opening/closing brace is not directly between '/'
|
||||||
|
const ErrInvalidPatternBracePosition = Error("capturing braces must be alone between slashes")
|
||||||
|
|
||||||
|
// ErrInvalidPatternOpeningBrace - a service pattern opening brace is invalid
|
||||||
|
const ErrInvalidPatternOpeningBrace = Error("opening brace already open")
|
||||||
|
|
||||||
|
// ErrInvalidPatternClosingBrace - a service pattern closing brace is invalid
|
||||||
|
const ErrInvalidPatternClosingBrace = Error("closing brace already closed")
|
||||||
|
|
||||||
|
// ErrMissingDescription - a service is missing its description
|
||||||
|
const ErrMissingDescription = Error("missing description")
|
||||||
|
|
||||||
|
// ErrMissingParamDesc - a parameter is missing its description
|
||||||
|
const ErrMissingParamDesc = Error("missing parameter description")
|
||||||
|
|
||||||
|
// ErrIllegalParamName - a parameter has an illegal name
|
||||||
|
const ErrIllegalParamName = Error("parameter name must not begin/end with '_'")
|
||||||
|
|
||||||
|
// ErrMissingParamType - a parameter has an illegal type
|
||||||
|
const ErrMissingParamType = Error("missing parameter type")
|
||||||
|
|
||||||
|
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
||||||
|
const ErrParamNameConflict = Error("name conflict for parameter")
|
|
@ -0,0 +1,22 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
func (param *Parameter) checkAndFormat() error {
|
||||||
|
|
||||||
|
// missing description
|
||||||
|
if len(param.Description) < 1 {
|
||||||
|
return ErrMissingParamDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid type
|
||||||
|
if len(param.Type) < 1 || param.Type == "?" {
|
||||||
|
return ErrMissingParamType
|
||||||
|
}
|
||||||
|
|
||||||
|
// set optional + type
|
||||||
|
if param.Type[0] == '?' {
|
||||||
|
param.Optional = true
|
||||||
|
param.Type = param.Type[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match returns if this service would handle this HTTP request
|
||||||
|
func (svc *Service) Match(req *http.Request) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) checkMethod() error {
|
||||||
|
for _, available := range availableHTTPMethods {
|
||||||
|
if svc.Method == available {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ErrUnknownMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) checkPattern() error {
|
||||||
|
length := len(svc.Pattern)
|
||||||
|
|
||||||
|
// empty pattern
|
||||||
|
if length < 1 {
|
||||||
|
return ErrInvalidPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > 1 {
|
||||||
|
// pattern not starting with '/' or ending with '/'
|
||||||
|
if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' {
|
||||||
|
return ErrInvalidPattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check capturing braces
|
||||||
|
depth := 0
|
||||||
|
for c, l := 1, length; c < l; c++ {
|
||||||
|
char := svc.Pattern[c]
|
||||||
|
|
||||||
|
if char == '{' {
|
||||||
|
// opening brace when already opened
|
||||||
|
if depth != 0 {
|
||||||
|
return ErrInvalidPatternOpeningBrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// not directly preceded by a slash
|
||||||
|
if svc.Pattern[c-1] != '/' {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *Service) checkAndFormatInput() error {
|
||||||
|
|
||||||
|
// ignore no parameter
|
||||||
|
if svc.Input == nil || len(svc.Input) < 1 {
|
||||||
|
svc.Input = make(map[string]*Parameter, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each parameter
|
||||||
|
for paramName, param := range svc.Input {
|
||||||
|
|
||||||
|
// fail on invalid name
|
||||||
|
if strings.Trim(paramName, "_") != paramName {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use param name if no rename
|
||||||
|
if len(param.Rename) < 1 {
|
||||||
|
param.Rename = paramName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := param.checkAndFormat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for name/rename conflict
|
||||||
|
for paramName2, param2 := range svc.Input {
|
||||||
|
// ignore self
|
||||||
|
if paramName == paramName2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.1. Same rename field
|
||||||
|
// 3.2.2. Not-renamed field matches a renamed field
|
||||||
|
// 3.2.3. Renamed field matches name
|
||||||
|
if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename {
|
||||||
|
return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse builds a server configuration from a json reader and checks for most format errors.
|
||||||
|
func Parse(r io.Reader) (Services, error) {
|
||||||
|
services := make(Services, 0)
|
||||||
|
|
||||||
|
err := json.NewDecoder(r).Decode(&services)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.checkAndFormat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if services.collide() {
|
||||||
|
return nil, fmt.Errorf("%s: %w", ErrFormat, ErrPatternCollision)
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collide returns if there is collision between services
|
||||||
|
func (svc Services) collide() bool {
|
||||||
|
// todo: implement pattern collision using types to check if braces can be equal to fixed uri parts
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a service matching an incoming HTTP request
|
||||||
|
func (svc Services) Find(r *http.Request) *Service {
|
||||||
|
for _, service := range svc {
|
||||||
|
if service.Match(r) {
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
||||||
|
func (svc Services) checkAndFormat() error {
|
||||||
|
for _, service := range svc {
|
||||||
|
|
||||||
|
// check method
|
||||||
|
err := service.checkMethod()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s '%s' [method]: %w", service.Method, service.Pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check pattern
|
||||||
|
service.Pattern = strings.Trim(service.Pattern, " \t\r\n")
|
||||||
|
err = service.checkPattern()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s '%s' [path]: %w", service.Method, service.Pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check description
|
||||||
|
if len(strings.Trim(service.Description, " \t\r\n")) < 1 {
|
||||||
|
return fmt.Errorf("%s '%s' [description]: %w", service.Method, service.Pattern, ErrMissingDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check input parameters
|
||||||
|
err = service.checkAndFormatInput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,29 +4,26 @@ import "net/http"
|
||||||
|
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
// Service represents a service definition (from api.json)
|
|
||||||
type Service struct {
|
|
||||||
GET *Method `json:"GET"`
|
|
||||||
POST *Method `json:"POST"`
|
|
||||||
PUT *Method `json:"PUT"`
|
|
||||||
DELETE *Method `json:"DELETE"`
|
|
||||||
|
|
||||||
Children map[string]*Service `json:"/"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Rename string `json:"name,omitempty"`
|
Rename string `json:"name,omitempty"`
|
||||||
Optional bool
|
Optional bool
|
||||||
Default *interface{} `json:"default"`
|
// Default *interface{} `json:"default"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method represents a method definition (from api.json)
|
// Service represents a service definition (from api.json)
|
||||||
type Method struct {
|
type Service struct {
|
||||||
Description string `json:"info"`
|
Method string `json:"method"`
|
||||||
|
Pattern string `json:"path"`
|
||||||
Scope [][]string `json:"scope"`
|
Scope [][]string `json:"scope"`
|
||||||
Parameters map[string]*Parameter `json:"in"`
|
Description string `json:"info"`
|
||||||
Download *bool `json:"download"`
|
Download *bool `json:"download"`
|
||||||
|
Input map[string]*Parameter `json:"in"`
|
||||||
|
// Output map[string]*Parameter `json:"out"`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services contains every service that represents a server configuration
|
||||||
|
type Services []*Service
|
|
@ -1,636 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLegalServiceName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Raw string
|
|
||||||
Error error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
`{
|
|
||||||
"/": {
|
|
||||||
"invalid/service-name": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service-name")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`{
|
|
||||||
"/": {
|
|
||||||
"invalid/service/name": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid/service/name")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
`{
|
|
||||||
"/": {
|
|
||||||
"invalid-service-name": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrIllegalServiceName.WrapString("invalid-service-name")),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
`{
|
|
||||||
"/": {
|
|
||||||
"valid.service_name": {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
if err != nil && test.Error == nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && test.Error != nil {
|
|
||||||
if err.Error() != test.Error.Error() {
|
|
||||||
t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestAvailableMethods(t *testing.T) {
|
|
||||||
reader := strings.NewReader(`{
|
|
||||||
"GET": { "info": "info" },
|
|
||||||
"POST": { "info": "info" },
|
|
||||||
"PUT": { "info": "info" },
|
|
||||||
"DELETE": { "info": "info" }
|
|
||||||
}`)
|
|
||||||
srv, err := Parse(reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error (got '%s')", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.Method(http.MethodGet) == nil {
|
|
||||||
t.Errorf("expected method GET to be available")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if srv.Method(http.MethodPost) == nil {
|
|
||||||
t.Errorf("expected method POST to be available")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if srv.Method(http.MethodPut) == nil {
|
|
||||||
t.Errorf("expected method PUT to be available")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if srv.Method(http.MethodDelete) == nil {
|
|
||||||
t.Errorf("expected method DELETE to be available")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.Method(http.MethodPatch) != nil {
|
|
||||||
t.Errorf("expected method PATH to be UNavailable")
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestParseEmpty(t *testing.T) {
|
|
||||||
reader := strings.NewReader(`{}`)
|
|
||||||
_, err := Parse(reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error (got '%s')", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestParseJsonError(t *testing.T) {
|
|
||||||
reader := strings.NewReader(`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info
|
|
||||||
},
|
|
||||||
}`) // trailing ',' is invalid JSON
|
|
||||||
_, err := Parse(reader)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseMissingMethodDescription(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Raw string
|
|
||||||
Error error
|
|
||||||
}{
|
|
||||||
{ // missing description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")),
|
|
||||||
},
|
|
||||||
{ // missing description
|
|
||||||
`{
|
|
||||||
"/": {
|
|
||||||
"subservice": {
|
|
||||||
"GET": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET subservice")),
|
|
||||||
},
|
|
||||||
{ // empty description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": ""
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingMethodDesc.WrapString("GET /")),
|
|
||||||
},
|
|
||||||
{ // valid description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "a"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // valid description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "some description"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
if err != nil && test.Error == nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && test.Error != nil {
|
|
||||||
if err.Error() != test.Error.Error() {
|
|
||||||
t.Errorf("expected the error '%s' (got '%s')", test.Error.Error(), err.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
|
||||||
reader := strings.NewReader(`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"original": { "info": "valid-desc", "type": "valid-type", "name": "" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
srv, err := Parse(reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
method := srv.Method(http.MethodGet)
|
|
||||||
if method == nil {
|
|
||||||
t.Errorf("expected GET method not to be nil")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
for _, param := range method.Parameters {
|
|
||||||
|
|
||||||
if param.Rename != "original" {
|
|
||||||
t.Errorf("expected the parameter 'original' not to be renamed to '%s'", param.Rename)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestOptionalParam(t *testing.T) {
|
|
||||||
reader := strings.NewReader(`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"optional": { "info": "valid-desc", "type": "?optional-type" },
|
|
||||||
"required": { "info": "valid-desc", "type": "required-type" },
|
|
||||||
"required2": { "info": "valid-desc", "type": "a" },
|
|
||||||
"optional2": { "info": "valid-desc", "type": "?a" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
srv, err := Parse(reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
method := srv.Method(http.MethodGet)
|
|
||||||
if method == nil {
|
|
||||||
t.Errorf("expected GET method not to be nil")
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
for pName, param := range method.Parameters {
|
|
||||||
|
|
||||||
if pName == "optional" || pName == "optional2" {
|
|
||||||
if !param.Optional {
|
|
||||||
t.Errorf("expected parameter '%s' to be optional", pName)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pName == "required" || pName == "required2" {
|
|
||||||
if param.Optional {
|
|
||||||
t.Errorf("expected parameter '%s' to be required", pName)
|
|
||||||
t.Failed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestParseParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Raw string
|
|
||||||
Error error
|
|
||||||
ErrorAlternative error
|
|
||||||
}{
|
|
||||||
{ // invalid param name prefix
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"_param1": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {_param1}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // invalid param name suffix
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1_": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrIllegalParamName.WrapString("GET / {param1_}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // missing param description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // empty param description
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingParamDesc.WrapString("GET / {param1}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // missing param type
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": "valid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // empty param type
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": "valid",
|
|
||||||
"type": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // invalid type (optional mark only)
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": "valid",
|
|
||||||
"type": "?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
|
|
||||||
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
|
||||||
ErrFormat.Wrap(ErrMissingParamType.WrapString("GET / {param1}")),
|
|
||||||
},
|
|
||||||
{ // valid description + valid type
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": "valid",
|
|
||||||
"type": "a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{ // valid description + valid OPTIONAL type
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": {
|
|
||||||
"info": "valid",
|
|
||||||
"type": "?valid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // name conflict with rename
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": { "info": "valid", "type": "valid" },
|
|
||||||
"param2": { "info": "valid", "type": "valid", "name": "param1" }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
// 2 possible errors as map order is not deterministic
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
|
||||||
},
|
|
||||||
{ // rename conflict with name
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": { "info": "valid", "type": "valid", "name": "param2" },
|
|
||||||
"param2": { "info": "valid", "type": "valid" }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
// 2 possible errors as map order is not deterministic
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
|
||||||
},
|
|
||||||
{ // rename conflict with rename
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": { "info": "valid", "type": "valid", "name": "conflict" },
|
|
||||||
"param2": { "info": "valid", "type": "valid", "name": "conflict" }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
// 2 possible errors as map order is not deterministic
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param1}")),
|
|
||||||
ErrFormat.Wrap(ErrParamNameConflict.WrapString("GET / {param2}")),
|
|
||||||
},
|
|
||||||
|
|
||||||
{ // both renamed with no conflict
|
|
||||||
`{
|
|
||||||
"GET": {
|
|
||||||
"info": "info",
|
|
||||||
"in": {
|
|
||||||
"param1": { "info": "valid", "type": "valid", "name": "freename" },
|
|
||||||
"param2": { "info": "valid", "type": "valid", "name": "freename2" }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
if err != nil && test.Error == nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && test.Error != nil {
|
|
||||||
if err.Error() != test.Error.Error() && err.Error() != test.ErrorAlternative.Error() {
|
|
||||||
t.Errorf("got the error: '%s'", err.Error())
|
|
||||||
t.Errorf("expected error (alternative 1): '%s'", test.Error.Error())
|
|
||||||
t.Errorf("expected error (alternative 2): '%s'", test.ErrorAlternative.Error())
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBrowseSimple(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Raw string
|
|
||||||
Path []string
|
|
||||||
BrowseDepth int
|
|
||||||
ValidDepth bool
|
|
||||||
}{
|
|
||||||
{ // false positive -1
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"parent", "subdir"},
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{ // false positive +1
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"parent", "subdir"},
|
|
||||||
3,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"parent", "subdir"},
|
|
||||||
2,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{ // unknown path
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"x", "y"},
|
|
||||||
2,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{ // unknown path
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"parent", "y"},
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{ // Warning: this case is important to understand the precedence of service paths over
|
|
||||||
// the value of some variables. Here if we send a string parameter in the GET method that
|
|
||||||
// unfortunately is equal to 'subdir', it will call the sub-service /parent/subdir' instead
|
|
||||||
// of the service /parent with its parameter set to the value 'subdir'.
|
|
||||||
`{
|
|
||||||
"/" : {
|
|
||||||
"parent": {
|
|
||||||
"/": {
|
|
||||||
"subdir": {}
|
|
||||||
},
|
|
||||||
"GET": {
|
|
||||||
"info": "valid-desc",
|
|
||||||
"in": {
|
|
||||||
"some-value": {
|
|
||||||
"info": "valid-desc",
|
|
||||||
"type": "valid-type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
[]string{"parent", "subdir"},
|
|
||||||
2,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
|
||||||
srv, err := Parse(strings.NewReader(test.Raw))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, depth := srv.Browse(test.Path)
|
|
||||||
if test.ValidDepth {
|
|
||||||
if depth != test.BrowseDepth {
|
|
||||||
t.Errorf("expected a depth of %d (got %d)", test.BrowseDepth, depth)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if depth == test.BrowseDepth {
|
|
||||||
t.Errorf("expected a depth NOT %d (got %d)", test.BrowseDepth, depth)
|
|
||||||
t.FailNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import "git.xdrm.io/go/aicra/internal/cerr"
|
|
||||||
|
|
||||||
// ErrRead - a problem ocurred when trying to read the configuration file
|
|
||||||
const ErrRead = cerr.Error("cannot read config")
|
|
||||||
|
|
||||||
// ErrFormat - a invalid format has been detected
|
|
||||||
const ErrFormat = cerr.Error("invalid config format")
|
|
||||||
|
|
||||||
// ErrIllegalServiceName - an illegal character has been found in a service name
|
|
||||||
const ErrIllegalServiceName = cerr.Error("service must not contain any slash '/' nor '-' symbols")
|
|
||||||
|
|
||||||
// ErrMissingMethodDesc - a method is missing its description
|
|
||||||
const ErrMissingMethodDesc = cerr.Error("missing method description")
|
|
||||||
|
|
||||||
// ErrMissingParamDesc - a parameter is missing its description
|
|
||||||
const ErrMissingParamDesc = cerr.Error("missing parameter description")
|
|
||||||
|
|
||||||
// ErrIllegalParamName - a parameter has an illegal name
|
|
||||||
const ErrIllegalParamName = cerr.Error("illegal parameter name (must not begin/end with '_')")
|
|
||||||
|
|
||||||
// ErrMissingParamType - a parameter has an illegal type
|
|
||||||
const ErrMissingParamType = cerr.Error("missing parameter type")
|
|
||||||
|
|
||||||
// ErrParamNameConflict - a parameter has a conflict with its name/rename field
|
|
||||||
const ErrParamNameConflict = cerr.Error("name conflict for parameter")
|
|
|
@ -1,69 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
|
||||||
func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) error {
|
|
||||||
|
|
||||||
// 1. fail on missing description
|
|
||||||
if len(methodDef.Description) < 1 {
|
|
||||||
return ErrMissingMethodDesc.WrapString(httpMethod + " " + servicePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. stop if no parameter
|
|
||||||
if methodDef.Parameters == nil || len(methodDef.Parameters) < 1 {
|
|
||||||
methodDef.Parameters = make(map[string]*Parameter, 0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. for each parameter
|
|
||||||
for pName, pData := range methodDef.Parameters {
|
|
||||||
|
|
||||||
// 3.1. check name
|
|
||||||
if strings.Trim(pName, "_") != pName {
|
|
||||||
return ErrIllegalParamName.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pData.Rename) < 1 {
|
|
||||||
pData.Rename = pName
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Check for name/rename conflict
|
|
||||||
for paramName, param := range methodDef.Parameters {
|
|
||||||
|
|
||||||
// ignore self
|
|
||||||
if pName == paramName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2.1. Same rename field
|
|
||||||
// 3.2.2. Not-renamed field matches a renamed field
|
|
||||||
// 3.2.3. Renamed field matches name
|
|
||||||
if pData.Rename == param.Rename || pName == param.Rename || pData.Rename == paramName {
|
|
||||||
return ErrParamNameConflict.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.3. Fail on missing description
|
|
||||||
if len(pData.Description) < 1 {
|
|
||||||
return ErrMissingParamDesc.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.4. Manage invalid type
|
|
||||||
if len(pData.Type) < 1 || pData.Type == "?" {
|
|
||||||
return ErrMissingParamType.WrapString(httpMethod + " " + servicePath + " {" + pName + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.5. Set optional + type
|
|
||||||
if pData.Type[0] == '?' {
|
|
||||||
pData.Optional = true
|
|
||||||
pData.Type = pData.Type[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse builds a service from a json reader and checks for most format errors.
|
|
||||||
func Parse(r io.Reader) (*Service, error) {
|
|
||||||
receiver := &Service{}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r).Decode(receiver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrRead.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = receiver.checkAndFormat("/")
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrFormat.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return receiver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method returns the actual method from the http method.
|
|
||||||
func (svc *Service) Method(httpMethod string) *Method {
|
|
||||||
httpMethod = strings.ToUpper(httpMethod)
|
|
||||||
|
|
||||||
switch httpMethod {
|
|
||||||
case http.MethodGet:
|
|
||||||
return svc.GET
|
|
||||||
case http.MethodPost:
|
|
||||||
return svc.POST
|
|
||||||
case http.MethodPut:
|
|
||||||
return svc.PUT
|
|
||||||
case http.MethodDelete:
|
|
||||||
return svc.DELETE
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browse the service childtree and returns the deepest matching child. The `path` is a formatted URL split by '/'
|
|
||||||
func (svc *Service) Browse(path []string) (*Service, int) {
|
|
||||||
currentService := svc
|
|
||||||
var depth int
|
|
||||||
|
|
||||||
// for each URI depth
|
|
||||||
for depth = 0; depth < len(path); depth++ {
|
|
||||||
currentPath := path[depth]
|
|
||||||
|
|
||||||
child, exists := currentService.Children[currentPath]
|
|
||||||
if !exists {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
currentService = child
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentService, depth
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
|
||||||
func (svc *Service) checkAndFormat(servicePath string) error {
|
|
||||||
|
|
||||||
// 1. check and format every method
|
|
||||||
for _, httpMethod := range availableHTTPMethods {
|
|
||||||
methodDef := svc.Method(httpMethod)
|
|
||||||
if methodDef == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := methodDef.checkAndFormat(servicePath, httpMethod)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. stop if no child */
|
|
||||||
if svc.Children == nil || len(svc.Children) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. for each service */
|
|
||||||
for childService, ctl := range svc.Children {
|
|
||||||
|
|
||||||
// 3.1. invalid name */
|
|
||||||
if strings.ContainsAny(childService, "/-") {
|
|
||||||
return ErrIllegalServiceName.WrapString(childService)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. check recursively */
|
|
||||||
err := ctl.checkAndFormat(childService)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue