2020-03-14 14:24:17 +00:00
package config
import (
"encoding/json"
"fmt"
"io"
"net/http"
2020-04-04 10:42:18 +00:00
"strings"
2020-03-14 23:27:54 +00:00
2020-03-16 08:20:00 +00:00
"git.xdrm.io/go/aicra/datatype"
2020-03-14 14:24:17 +00:00
)
2020-04-04 09:45:49 +00:00
// Server definition
type Server struct {
Types [ ] datatype . T
Services [ ] * Service
}
2020-03-14 14:24:17 +00:00
2020-04-04 09:45:49 +00:00
// Parse a reader into a server. Server.Types must be set beforehand to
// make datatypes available when checking and formatting the read configuration.
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 )
2020-03-14 14:24:17 +00:00
}
2020-04-04 09:45:49 +00:00
if err := srv . validate ( ) ; err != nil {
return fmt . Errorf ( "%s: %w" , ErrFormat , err )
2020-03-14 14:24:17 +00:00
}
2020-04-04 09:45:49 +00:00
return nil
2020-03-28 11:31:44 +00:00
}
2020-04-04 09:45:49 +00:00
// validate implements the validator interface
func ( server Server ) validate ( datatypes ... datatype . T ) error {
2020-03-28 11:31:44 +00:00
for _ , service := range server . Services {
2020-04-04 09:45:49 +00:00
err := service . validate ( server . Types ... )
2020-03-28 11:31:44 +00:00
if err != nil {
return fmt . Errorf ( "%s '%s': %w" , service . Method , service . Pattern , err )
}
}
// check for collisions
2020-03-14 23:27:54 +00:00
if err := server . collide ( ) ; err != nil {
2020-03-28 11:31:44 +00:00
return fmt . Errorf ( "%s: %w" , ErrFormat , err )
2020-03-14 14:24:17 +00:00
}
2020-03-28 11:31:44 +00:00
return nil
2020-03-14 14:24:17 +00:00
}
2020-03-16 09:56:26 +00:00
// 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
}
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 {
2020-03-16 08:01:51 +00:00
length := len ( server . Services )
2020-03-14 23:27:54 +00:00
// for each service combination
for a := 0 ; a < length ; a ++ {
for b := a + 1 ; b < length ; b ++ {
2020-03-16 08:01:51 +00:00
aService := server . Services [ a ]
bService := server . Services [ b ]
2020-03-14 23:27:54 +00:00
// ignore different method
if aService . Method != bService . Method {
continue
}
2020-03-16 08:26:10 +00:00
aParts := SplitURL ( aService . Pattern )
bParts := SplitURL ( bService . Pattern )
2020-03-14 23:27:54 +00:00
// not same size
if len ( aParts ) != len ( bParts ) {
continue
}
2020-03-21 14:48:59 +00:00
partErrors := make ( [ ] error , 0 )
2020-03-14 23:27:54 +00:00
// 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 {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (path %s and %s)" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , aPart , bPart ) )
2020-03-21 14:48:59 +00:00
continue
2020-03-14 23:27:54 +00:00
}
// no capture -> check equal
if ! aIsCapture && ! bIsCapture {
if aPart == bPart {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (same path '%s')" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , aPart ) )
2020-03-21 14:58:05 +00:00
continue
2020-03-14 23:27:54 +00:00
}
}
// 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 {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (invalid type for %s)" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , aPart ) )
2020-03-21 14:48:59 +00:00
continue
2020-03-14 23:27:54 +00:00
}
// fail if not valid
2020-03-21 14:48:59 +00:00
if _ , valid := input . Validator ( bPart ) ; valid {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (%s captures '%s')" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , aPart , bPart ) )
2020-03-21 14:48:59 +00:00
continue
2020-03-14 23:27:54 +00:00
}
// B captures A -> check type (A is B ?)
2020-03-21 14:58:05 +00:00
} else if bIsCapture {
2020-03-14 23:27:54 +00:00
input , exists := bService . Input [ bPart ]
// fail if no type or no validator
if ! exists || input . Validator == nil {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (invalid type for %s)" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , bPart ) )
2020-03-21 14:48:59 +00:00
continue
2020-03-14 23:27:54 +00:00
}
// fail if not valid
2020-03-21 14:48:59 +00:00
if _ , valid := input . Validator ( aPart ) ; valid {
2020-03-21 14:57:35 +00:00
partErrors = append ( partErrors , fmt . Errorf ( "(%s '%s') vs (%s '%s'): %w (%s captures '%s')" , aService . Method , aService . Pattern , bService . Method , bService . Pattern , ErrPatternCollision , bPart , aPart ) )
2020-03-21 14:48:59 +00:00
continue
2020-03-14 23:27:54 +00:00
}
}
2020-03-21 14:48:59 +00:00
partErrors = append ( partErrors , nil )
}
// if at least 1 url part does not match -> ok
var firstError error
oneMismatch := false
for _ , err := range partErrors {
if err != nil && firstError == nil {
firstError = err
}
if err == nil {
oneMismatch = true
continue
}
}
if ! oneMismatch {
return firstError
2020-03-14 23:27:54 +00:00
}
}
}
return nil
2020-03-14 14:24:17 +00:00
}
2020-04-04 10:42:18 +00:00
// SplitURL without empty sets
func SplitURL ( url string ) [ ] string {
trimmed := strings . Trim ( url , " /\t\r\n" )
split := strings . Split ( trimmed , "/" )
// remove empty set when empty url
if len ( split ) == 1 && len ( split [ 0 ] ) == 0 {
return [ ] string { }
}
return split
}