package config import ( "encoding/json" "fmt" "io" "net/http" "git.xdrm.io/go/aicra/datatype" ) // Parse builds a server configuration from a json reader and checks for most format errors. // you can provide additional DataTypes as variadic arguments func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) { server := &Server{ Types: make([]datatype.T, 0), Services: make([]*Service, 0), } // add data types for _, dtype := range dtypes { server.Types = append(server.Types, dtype) } if err := json.NewDecoder(r).Decode(&server.Services); err != nil { return nil, fmt.Errorf("%s: %w", ErrRead, err) } if err := server.Validate(); err != 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 { for _, service := range server.Services { err := service.Validate(server.Types...) if err != nil { return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err) } } // check for collisions if err := server.collide(); err != nil { return fmt.Errorf("%s: %w", ErrFormat, err) } return 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 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 } partErrors := make([]error, 0) // 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 { 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)) continue } // no capture -> check equal if !aIsCapture && !bIsCapture { if aPart == bPart { partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (same path '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart)) 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 { 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)) continue } // fail if not valid if _, valid := input.Validator(bPart); valid { 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)) continue } // B captures A -> check type (A is B ?) } else if bIsCapture { input, exists := bService.Input[bPart] // fail if no type or no validator if !exists || input.Validator == nil { 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)) continue } // fail if not valid if _, valid := input.Validator(aPart); valid { 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)) continue } } 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 } } } return nil }