mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-23 09:31:02 +01:00
- add block syntax parser/scanner with nested @blocks and elif/else support - restructure rule execution into explicit pre/post phases with phase flags - classify commands by phase and termination behavior - enforce flow semantics (default rule handling, dead-rule detection) - expand HTTP flow coverage with block + YAML parity tests and benches - refresh rules README/spec and update playground/docs integration
387 lines
9.0 KiB
Go
387 lines
9.0 KiB
Go
package rules
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"unicode"
|
|
|
|
gperr "github.com/yusing/goutils/errs"
|
|
httputils "github.com/yusing/goutils/http"
|
|
)
|
|
|
|
// IfBlockCommand is an inline conditional block inside a do-body.
|
|
//
|
|
// Syntax (within a rule do block):
|
|
//
|
|
// @<on-expr> { <do...> }
|
|
//
|
|
// Semantics:
|
|
// - Evaluated in the same phase the parent rule runs.
|
|
// - If <on-expr> matches, run the nested commands in-order.
|
|
// - Otherwise do nothing.
|
|
//
|
|
// NOTE: Per current design decision, we keep this permissive:
|
|
// nested blocks may use response matchers and response commands; no extra phase validation is performed.
|
|
type IfBlockCommand struct {
|
|
On RuleOn
|
|
Do []CommandHandler
|
|
}
|
|
|
|
func (c IfBlockCommand) ServeHTTP(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
if c.Do == nil {
|
|
return nil
|
|
}
|
|
// If On.checker is nil, treat as unconditional (should not happen if parsed).
|
|
if c.On.checker == nil {
|
|
return Commands(c.Do).ServeHTTP(w, r, upstream)
|
|
}
|
|
if c.On.checker.Check(w, r) {
|
|
return Commands(c.Do).ServeHTTP(w, r, upstream)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c IfBlockCommand) Phase() PhaseFlag {
|
|
phase := c.On.phase
|
|
for _, cmd := range c.Do {
|
|
phase |= cmd.Phase()
|
|
}
|
|
return phase
|
|
}
|
|
|
|
// IfElseBlockCommand is a chained conditional block inside a do-body.
|
|
//
|
|
// Syntax (within a rule do block):
|
|
//
|
|
// @<on-expr> { <do...> } elif <on-expr> { <do...> } ... else { <do...> }
|
|
//
|
|
// NOTE: `elif`/`else` must appear on the same line as the preceding closing brace (`}`),
|
|
// e.g. `} elif ... {` and `} else {`.
|
|
type IfElseBlockCommand struct {
|
|
Ifs []IfBlockCommand
|
|
Else []CommandHandler
|
|
}
|
|
|
|
func (c IfElseBlockCommand) ServeHTTP(w *httputils.ResponseModifier, r *http.Request, upstream http.HandlerFunc) error {
|
|
for _, br := range c.Ifs {
|
|
// If On.checker is nil, treat as unconditional.
|
|
if br.On.checker == nil {
|
|
if br.Do == nil {
|
|
continue
|
|
}
|
|
return Commands(br.Do).ServeHTTP(w, r, upstream)
|
|
}
|
|
if br.Do == nil {
|
|
continue
|
|
}
|
|
if br.On.checker.Check(w, r) {
|
|
return Commands(br.Do).ServeHTTP(w, r, upstream)
|
|
}
|
|
}
|
|
if len(c.Else) > 0 {
|
|
return Commands(c.Else).ServeHTTP(w, r, upstream)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c IfElseBlockCommand) Phase() PhaseFlag {
|
|
phase := PhaseNone
|
|
for _, br := range c.Ifs {
|
|
phase |= br.Phase()
|
|
}
|
|
if len(c.Else) > 0 {
|
|
phase |= Commands(c.Else).Phase()
|
|
}
|
|
return phase
|
|
}
|
|
|
|
func skipSameLineSpace(src string, pos int) int {
|
|
for pos < len(src) {
|
|
switch src[pos] {
|
|
case '\n':
|
|
return pos
|
|
case '\r':
|
|
pos++
|
|
continue
|
|
case ' ', '\t':
|
|
pos++
|
|
continue
|
|
default:
|
|
return pos
|
|
}
|
|
}
|
|
return pos
|
|
}
|
|
|
|
func parseAtBlockChain(src string, atPos int) (CommandHandler, int, error) {
|
|
length := len(src)
|
|
headerStart := atPos + 1
|
|
|
|
parseBranch := func(onExpr string, bodyStart int, bodyEnd int) (RuleOn, []CommandHandler, error) {
|
|
var on RuleOn
|
|
if err := on.Parse(onExpr); err != nil {
|
|
return RuleOn{}, nil, err
|
|
}
|
|
innerSrc := ""
|
|
if bodyStart < bodyEnd {
|
|
innerSrc = src[bodyStart:bodyEnd]
|
|
}
|
|
inner, err := parseDoWithBlocks(innerSrc)
|
|
if err != nil {
|
|
return RuleOn{}, nil, err
|
|
}
|
|
if len(inner) == 0 {
|
|
return on, nil, nil
|
|
}
|
|
return on, inner, nil
|
|
}
|
|
|
|
onExpr, bracePos, herr := parseHeaderToBrace(src, headerStart)
|
|
if herr != nil {
|
|
return nil, 0, herr
|
|
}
|
|
if bracePos >= length || src[bracePos] != '{' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after nested block header")
|
|
}
|
|
|
|
// Parse first @<on-expr> { ... }
|
|
p := bracePos
|
|
bodyStart := p + 1
|
|
bodyEnd, ferr := findMatchingBrace(src, &p, bodyStart)
|
|
if ferr != nil {
|
|
return nil, 0, ferr
|
|
}
|
|
firstOn, firstDo, berr := parseBranch(onExpr, bodyStart, bodyEnd)
|
|
if berr != nil {
|
|
return nil, 0, berr
|
|
}
|
|
|
|
ifs := []IfBlockCommand{{On: firstOn, Do: firstDo}}
|
|
var elseDo []CommandHandler
|
|
hasChain := false
|
|
hasElse := false
|
|
|
|
for {
|
|
q := skipSameLineSpace(src, p)
|
|
if q >= length || src[q] == '\n' {
|
|
break
|
|
}
|
|
|
|
// elif <on-expr> { ... }
|
|
if strings.HasPrefix(src[q:], "elif") {
|
|
next := q + len("elif")
|
|
if next >= length {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr after 'elif'")
|
|
}
|
|
if src[next] == '\n' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr after 'elif'")
|
|
}
|
|
if !unicode.IsSpace(rune(src[next])) {
|
|
if src[next] == '{' || src[next] == '}' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr after 'elif'")
|
|
}
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected whitespace after 'elif'")
|
|
}
|
|
next++
|
|
for next < length {
|
|
c := src[next]
|
|
if c == '\n' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after elif condition")
|
|
}
|
|
if c == '\r' {
|
|
next++
|
|
continue
|
|
}
|
|
if !unicode.IsSpace(rune(c)) {
|
|
break
|
|
}
|
|
next++
|
|
}
|
|
|
|
p2 := next
|
|
elifOnExpr, bracePos, herr := parseHeaderToBrace(src, p2)
|
|
if herr != nil {
|
|
return nil, 0, herr
|
|
}
|
|
if elifOnExpr == "" {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected on-expr after 'elif'")
|
|
}
|
|
if bracePos >= length || src[bracePos] != '{' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after elif condition")
|
|
}
|
|
p2 = bracePos
|
|
elifBodyStart := p2 + 1
|
|
elifBodyEnd, ferr := findMatchingBrace(src, &p2, elifBodyStart)
|
|
if ferr != nil {
|
|
return nil, 0, ferr
|
|
}
|
|
elifOn, elifDo, berr := parseBranch(elifOnExpr, elifBodyStart, elifBodyEnd)
|
|
if berr != nil {
|
|
return nil, 0, berr
|
|
}
|
|
ifs = append(ifs, IfBlockCommand{On: elifOn, Do: elifDo})
|
|
hasChain = true
|
|
p = p2
|
|
continue
|
|
}
|
|
|
|
// else { ... }
|
|
if strings.HasPrefix(src[q:], "else") {
|
|
if hasElse {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("multiple 'else' branches")
|
|
}
|
|
next := q + len("else")
|
|
for next < length {
|
|
c := src[next]
|
|
if c == '\n' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after 'else'")
|
|
}
|
|
if c == '\r' {
|
|
next++
|
|
continue
|
|
}
|
|
if !unicode.IsSpace(rune(c)) {
|
|
break
|
|
}
|
|
next++
|
|
}
|
|
if next >= length || src[next] != '{' {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("expected '{' after 'else'")
|
|
}
|
|
|
|
elseBodyStart := next + 1
|
|
p2 := next
|
|
elseBodyEnd, ferr := findMatchingBrace(src, &p2, elseBodyStart)
|
|
if ferr != nil {
|
|
return nil, 0, ferr
|
|
}
|
|
innerSrc := ""
|
|
if elseBodyStart < elseBodyEnd {
|
|
innerSrc = src[elseBodyStart:elseBodyEnd]
|
|
}
|
|
inner, ierr := parseDoWithBlocks(innerSrc)
|
|
if ierr != nil {
|
|
return nil, 0, ierr
|
|
}
|
|
if len(inner) == 0 {
|
|
elseDo = nil
|
|
} else {
|
|
elseDo = inner
|
|
}
|
|
hasChain = true
|
|
hasElse = true
|
|
p = p2
|
|
|
|
// else must be the last branch on that line.
|
|
for q2 := skipSameLineSpace(src, p); q2 < length && src[q2] != '\n'; q2 = skipSameLineSpace(src, q2) {
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("unexpected token after else block")
|
|
}
|
|
break
|
|
}
|
|
|
|
return nil, 0, ErrInvalidBlockSyntax.Withf("unexpected token after nested block; expected 'elif'/'else' or newline")
|
|
}
|
|
|
|
if hasChain {
|
|
return IfElseBlockCommand{Ifs: ifs, Else: elseDo}, p, nil
|
|
}
|
|
return IfBlockCommand{On: ifs[0].On, Do: ifs[0].Do}, p, nil
|
|
}
|
|
|
|
// parseDoWithBlocks parses a do-body containing plain command lines and nested @-blocks.
|
|
// It returns the outer command handlers and the require phase.
|
|
//
|
|
// A nested block is only recognized when '@' is the first non-space character on a line.
|
|
func parseDoWithBlocks(src string) (handlers []CommandHandler, err error) {
|
|
pos := 0
|
|
length := len(src)
|
|
lineStart := true
|
|
handlers = make([]CommandHandler, 0, strings.Count(src, "\n")+1)
|
|
|
|
appendLineCommand := func(line string) error {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
directive, args, err := parse(line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
builder, ok := commands[directive]
|
|
if !ok {
|
|
return ErrUnknownDirective.Subject(directive)
|
|
}
|
|
|
|
phase, validArgs, err := builder.validate(args)
|
|
if err != nil {
|
|
return gperr.PrependSubject(err, directive).With(builder.help.Error())
|
|
}
|
|
|
|
h := builder.build(validArgs)
|
|
handlers = append(handlers, Handler{fn: h, phase: phase, terminate: builder.terminate})
|
|
return nil
|
|
}
|
|
|
|
for pos < length {
|
|
// Handle newlines
|
|
switch src[pos] {
|
|
case '\n':
|
|
pos++
|
|
lineStart = true
|
|
continue
|
|
case '\r':
|
|
// tolerate CRLF
|
|
pos++
|
|
continue
|
|
}
|
|
|
|
if lineStart {
|
|
// Find first non-space on the line.
|
|
linePos := pos
|
|
for linePos < length {
|
|
c := rune(src[linePos])
|
|
if c == '\n' {
|
|
break
|
|
}
|
|
if !unicode.IsSpace(c) {
|
|
break
|
|
}
|
|
linePos++
|
|
}
|
|
|
|
if linePos < length && src[linePos] == '@' {
|
|
h, next, err := parseAtBlockChain(src, linePos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handlers = append(handlers, h)
|
|
pos = next
|
|
lineStart = false
|
|
continue
|
|
}
|
|
|
|
// Not a nested block; parse the rest of this line as a command.
|
|
lineEnd := pos
|
|
for lineEnd < length && src[lineEnd] != '\n' {
|
|
lineEnd++
|
|
}
|
|
if lerr := appendLineCommand(src[pos:lineEnd]); lerr != nil {
|
|
return nil, lerr
|
|
}
|
|
pos = lineEnd
|
|
lineStart = true
|
|
continue
|
|
}
|
|
|
|
// Not at line start; advance to the next line boundary.
|
|
for pos < length && src[pos] != '\n' {
|
|
pos++
|
|
}
|
|
lineStart = true
|
|
}
|
|
|
|
return handlers, nil
|
|
}
|