2020-03-14 14:24:17 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
2020-03-14 23:27:54 +00:00
|
|
|
|
|
|
|
"git.xdrm.io/go/aicra/config/datatype"
|
2020-03-14 14:24:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Parse builds a server configuration from a json reader and checks for most format errors.
|
2020-03-14 23:27:54 +00:00
|
|
|
// you can provide additional DataTypes as variadic arguments
|
|
|
|
func Parse(r io.Reader, dtypes ...datatype.DataType) (*Server, error) {
|
|
|
|
server := &Server{
|
|
|
|
types: make([]datatype.DataType, 0),
|
|
|
|
services: make([]*Service, 0),
|
|
|
|
}
|
|
|
|
// add data types
|
|
|
|
for _, dtype := range dtypes {
|
|
|
|
server.types = append(server.types, dtype)
|
|
|
|
}
|
2020-03-14 14:24:17 +00:00
|
|
|
|
2020-03-14 23:27:54 +00:00
|
|
|
// parse JSON
|
|
|
|
if err := json.NewDecoder(r).Decode(&server.services); err != nil {
|
2020-03-14 14:24:17 +00:00
|
|
|
return nil, fmt.Errorf("%s: %w", ErrRead, err)
|
|
|
|
}
|
|
|
|
|
2020-03-14 23:27:54 +00:00
|
|
|
// check services
|
|
|
|
if err := server.checkAndFormat(); err != nil {
|
2020-03-14 14:24:17 +00:00
|
|
|
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
|
|
|
}
|
|
|
|
|
2020-03-14 23:27:54 +00:00
|
|
|
// check collisions
|
|
|
|
if err := server.collide(); err != nil {
|
|
|
|
return nil, fmt.Errorf("%s: %w", ErrFormat, err)
|
2020-03-14 14:24:17 +00:00
|
|
|
}
|
|
|
|
|
2020-03-14 23:27:54 +00:00
|
|
|
return server, nil
|
2020-03-14 14:24:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// collide returns if there is collision between services
|
2020-03-14 23:27:54 +00:00
|
|
|
func (server *Server) collide() error {
|
|
|
|
length := len(server.services)
|
|
|
|
|
|
|
|
// for each service combination
|
|
|
|
for a := 0; a < length; a++ {
|
|
|
|
for b := a + 1; b < length; b++ {
|
|
|
|
aService := server.services[a]
|
|
|
|
bService := server.services[b]
|
|
|
|
|
|
|
|
// ignore different method
|
|
|
|
if aService.Method != bService.Method {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
aParts := splitURL(aService.Pattern)
|
|
|
|
bParts := splitURL(bService.Pattern)
|
|
|
|
|
|
|
|
// not same size
|
|
|
|
if len(aParts) != len(bParts) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// for each part
|
|
|
|
for pi, aPart := range aParts {
|
|
|
|
bPart := bParts[pi]
|
|
|
|
|
|
|
|
aIsCapture := len(aPart) > 1 && aPart[0] == '{'
|
|
|
|
bIsCapture := len(bPart) > 1 && bPart[0] == '{'
|
|
|
|
|
|
|
|
// both captures -> as we cannot check, consider a collision
|
|
|
|
if aIsCapture && bIsCapture {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
|
|
|
|
// no capture -> check equal
|
|
|
|
if !aIsCapture && !bIsCapture {
|
|
|
|
if aPart == bPart {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// A captures B -> check type (B is A ?)
|
|
|
|
if aIsCapture {
|
|
|
|
input, exists := aService.Input[aPart]
|
|
|
|
|
|
|
|
// fail if no type or no validator
|
|
|
|
if !exists || input.Validator == nil {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
|
|
|
|
// fail if not valid
|
|
|
|
if _, valid := input.Validator(aPart); !valid {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
|
|
|
|
// B captures A -> check type (A is B ?)
|
|
|
|
} else {
|
|
|
|
input, exists := bService.Input[bPart]
|
|
|
|
|
|
|
|
// fail if no type or no validator
|
|
|
|
if !exists || input.Validator == nil {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
|
|
|
|
// fail if not valid
|
|
|
|
if _, valid := input.Validator(bPart); !valid {
|
|
|
|
return fmt.Errorf("%s: %s '%s'", ErrPatternCollision, aService.Method, aService.Pattern)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2020-03-14 14:24:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Find a service matching an incoming HTTP request
|
2020-03-14 23:27:54 +00:00
|
|
|
func (server Server) Find(r *http.Request) *Service {
|
|
|
|
for _, service := range server.services {
|
2020-03-14 14:24:17 +00:00
|
|
|
if service.Match(r) {
|
|
|
|
return service
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkAndFormat checks for errors and missing fields and sets default values for optional fields.
|
2020-03-14 23:27:54 +00:00
|
|
|
func (server Server) checkAndFormat() error {
|
|
|
|
for _, service := range server.services {
|
2020-03-14 14:24:17 +00:00
|
|
|
|
|
|
|
// 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
|
2020-03-14 23:27:54 +00:00
|
|
|
err = service.checkAndFormatInput(server.types)
|
2020-03-14 14:24:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%s '%s' [in]: %w", service.Method, service.Pattern, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|