test dynfunc package; standardize and refactor api #14
|
@ -80,7 +80,8 @@ func TestLegalServiceName(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -134,7 +135,8 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if test.ValidMethod && err != nil {
|
if test.ValidMethod && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err.Error())
|
t.Errorf("unexpected error: '%s'", err.Error())
|
||||||
|
@ -150,20 +152,22 @@ func TestAvailableMethods(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestParseEmpty(t *testing.T) {
|
func TestParseEmpty(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[]`)
|
r := strings.NewReader(`[]`)
|
||||||
_, err := Parse(reader)
|
srv := &Server{}
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error (got '%s')", err)
|
t.Errorf("unexpected error (got '%s')", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestParseJsonError(t *testing.T) {
|
func TestParseJsonError(t *testing.T) {
|
||||||
reader := strings.NewReader(`{
|
r := strings.NewReader(`{
|
||||||
"GET": {
|
"GET": {
|
||||||
"info": "info
|
"info": "info
|
||||||
},
|
},
|
||||||
}`) // trailing ',' is invalid JSON
|
}`) // trailing ',' is invalid JSON
|
||||||
_, err := Parse(reader)
|
srv := &Server{}
|
||||||
|
err := srv.Parse(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected error")
|
t.Errorf("expected error")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -205,7 +209,8 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw))
|
srv := &Server{}
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if test.ValidDescription && err != nil {
|
if test.ValidDescription && err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
@ -223,7 +228,7 @@ func TestParseMissingMethodDescription(t *testing.T) {
|
||||||
|
|
||||||
func TestParamEmptyRenameNoRename(t *testing.T) {
|
func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
r := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -233,7 +238,9 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv, err := Parse(reader, builtin.AnyDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -254,7 +261,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestOptionalParam(t *testing.T) {
|
func TestOptionalParam(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
reader := strings.NewReader(`[
|
r := strings.NewReader(`[
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
|
@ -267,7 +274,10 @@ func TestOptionalParam(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]`)
|
]`)
|
||||||
srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
|
err := srv.Parse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@ -577,7 +587,9 @@ func TestParseParameters(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Raw))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -814,7 +826,10 @@ func TestServiceCollision(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
_, err := Parse(strings.NewReader(test.Config), builtin.StringDataType{}, builtin.UintDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.StringDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.UintDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err == nil && test.Error != nil {
|
if err == nil && test.Error != nil {
|
||||||
t.Errorf("expected an error: '%s'", test.Error.Error())
|
t.Errorf("expected an error: '%s'", test.Error.Error())
|
||||||
|
@ -951,7 +966,11 @@ func TestMatchSimple(t *testing.T) {
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) {
|
||||||
srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{})
|
srv := &Server{}
|
||||||
|
srv.Types = append(srv.Types, builtin.AnyDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.IntDataType{})
|
||||||
|
srv.Types = append(srv.Types, builtin.BoolDataType{})
|
||||||
|
err := srv.Parse(strings.NewReader(test.Config))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: '%s'", err)
|
t.Errorf("unexpected error: '%s'", err)
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Parameter represents a parameter definition (from api.json)
|
||||||
func (param *Parameter) Validate(datatypes ...datatype.T) error {
|
type Parameter struct {
|
||||||
|
Description string `json:"info"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Rename string `json:"name,omitempty"`
|
||||||
|
// ExtractType is the type of data the datatype returns
|
||||||
|
ExtractType reflect.Type
|
||||||
|
// Optional is set to true when the type is prefixed with '?'
|
||||||
|
Optional bool
|
||||||
|
|
||||||
|
// Validator is inferred from @Type
|
||||||
|
Validator datatype.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (param *Parameter) validate(datatypes ...datatype.T) error {
|
||||||
// missing description
|
// missing description
|
||||||
if len(param.Description) < 1 {
|
if len(param.Description) < 1 {
|
||||||
return ErrMissingParamDesc
|
return ErrMissingParamDesc
|
||||||
|
|
|
@ -9,34 +9,30 @@ import (
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
"git.xdrm.io/go/aicra/datatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse builds a server configuration from a json reader and checks for most format errors.
|
// Server definition
|
||||||
// you can provide additional DataTypes as variadic arguments
|
type Server struct {
|
||||||
func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) {
|
Types []datatype.T
|
||||||
server := &Server{
|
Services []*Service
|
||||||
Types: make([]datatype.T, 0),
|
|
||||||
Services: make([]*Service, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add data types
|
// Parse a reader into a server. Server.Types must be set beforehand to
|
||||||
for _, dtype := range dtypes {
|
// make datatypes available when checking and formatting the read configuration.
|
||||||
server.Types = append(server.Types, dtype)
|
func (srv *Server) Parse(r io.Reader) error {
|
||||||
|
if err := json.NewDecoder(r).Decode(&srv.Services); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", ErrRead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r).Decode(&server.Services); err != nil {
|
if err := srv.validate(); err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
return fmt.Errorf("%s: %w", ErrFormat, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Validate(); err != nil {
|
return nil
|
||||||
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return server, nil
|
// validate implements the validator interface
|
||||||
}
|
func (server Server) validate(datatypes ...datatype.T) error {
|
||||||
|
|
||||||
// Validate implements the validator interface
|
|
||||||
func (server Server) Validate(datatypes ...datatype.T) error {
|
|
||||||
for _, service := range server.Services {
|
for _, service := range server.Services {
|
||||||
err := service.Validate(server.Types...)
|
err := service.validate(server.Types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
|
return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,35 @@ import (
|
||||||
|
|
||||||
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`)
|
||||||
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`)
|
||||||
|
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
||||||
|
|
||||||
|
// Service definition
|
||||||
|
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"`
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// BraceCapture links to the related URI parameter
|
||||||
|
type BraceCapture struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
Ref *Parameter
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -24,9 +53,6 @@ func (svc *Service) Match(req *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and extract input
|
|
||||||
// todo: check if input match and extract models
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +102,7 @@ func (svc *Service) matchPattern(uri string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate implements the validator interface
|
// Validate implements the validator interface
|
||||||
func (svc *Service) Validate(datatypes ...datatype.T) error {
|
func (svc *Service) validate(datatypes ...datatype.T) error {
|
||||||
// check method
|
// check method
|
||||||
err := svc.isMethodAvailable()
|
err := svc.isMethodAvailable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -233,7 +259,7 @@ func (svc *Service) validateInput(types []datatype.T) error {
|
||||||
param.Rename = paramName
|
param.Rename = paramName
|
||||||
}
|
}
|
||||||
|
|
||||||
err := param.Validate(types...)
|
err := param.validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
@ -283,7 +309,7 @@ func (svc *Service) validateOutput(types []datatype.T) error {
|
||||||
param.Rename = paramName
|
param.Rename = paramName
|
||||||
}
|
}
|
||||||
|
|
||||||
err := param.Validate(types...)
|
err := param.validate(types...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", paramName, err)
|
return fmt.Errorf("%s: %w", paramName, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"git.xdrm.io/go/aicra/datatype"
|
|
||||||
)
|
|
||||||
|
|
||||||
var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
||||||
|
|
||||||
// validator unifies the check and format routine
|
|
||||||
type validator interface {
|
|
||||||
Validate(...datatype.T) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
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)
|
|
||||||
type Parameter struct {
|
|
||||||
Description string `json:"info"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Rename string `json:"name,omitempty"`
|
|
||||||
// ExtractType is the type of data the datatype returns
|
|
||||||
ExtractType reflect.Type
|
|
||||||
// Optional is set to true when the type is prefixed with '?'
|
|
||||||
Optional bool
|
|
||||||
|
|
||||||
// Validator is inferred from @Type
|
|
||||||
Validator datatype.Validator
|
|
||||||
}
|
|
||||||
|
|
||||||
// BraceCapture links to the related URI parameter
|
|
||||||
type BraceCapture struct {
|
|
||||||
Name string
|
|
||||||
Index int
|
|
||||||
Ref *Parameter
|
|
||||||
}
|
|
Loading…
Reference in New Issue