mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 08:48:32 +02:00
refactor(rules): introduce block DSL, phase-based execution, and flow validation
- 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
This commit is contained in:
386
internal/route/rules/do_blocks.go
Normal file
386
internal/route/rules/do_blocks.go
Normal file
@@ -0,0 +1,386 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user