package filter import ( "fmt" "strings" "unicode" ) // Parse parses a filter expression string into an Expr tree. // Returns (nil, nil) for an empty filter string. // Bare words without a field operator are rejected; use ParseAndValidate // with a Schema that has DefaultFields set to allow them. func Parse(input string) (Expr, error) { return parseWith(input, nil) } func parseWith(input string, defaultFields []string) (Expr, error) { input = strings.TrimSpace(input) if input == "" { return nil, nil } p := &parser{input: input, defaultFields: defaultFields} expr, err := p.parseExpression() if err != nil { return nil, fmt.Errorf("%w: %s", ErrInvalid, err) } if !p.atEOF() { tok := p.peek() return nil, fmt.Errorf("%w: unexpected input %q at position %d", ErrInvalid, tok.value, tok.pos) } return expr, nil } // ---- Lexer ---- type tokenKind int const ( tokEOF tokenKind = iota tokText // field name or unquoted value tokString // "quoted string" tokAnd // AND (case-insensitive) tokOr // OR (case-insensitive) tokNot // NOT (case-insensitive) tokEq // = tokNe // != tokLt // < tokGt // > tokLe // <= tokGe // >= tokHas // : tokLParen // ( tokRParen // ) tokMinus // - ) type token struct { kind tokenKind value string pos int } // ---- Parser ---- type parser struct { input string pos int peeked *token defaultFields []string } func (p *parser) atEOF() bool { if p.peeked != nil { return p.peeked.kind == tokEOF } p.skipWS() return p.pos >= len(p.input) } func (p *parser) skipWS() { for p.pos < len(p.input) && unicode.IsSpace(rune(p.input[p.pos])) { p.pos++ } } func (p *parser) peek() token { if p.peeked != nil { return *p.peeked } tok := p.scan() p.peeked = &tok return tok } func (p *parser) consume() token { if p.peeked != nil { tok := *p.peeked p.peeked = nil return tok } return p.scan() } func (p *parser) scan() token { p.skipWS() if p.pos >= len(p.input) { return token{kind: tokEOF, pos: p.pos} } start := p.pos ch := p.input[p.pos] switch ch { case '(': p.pos++ return token{kind: tokLParen, pos: start} case ')': p.pos++ return token{kind: tokRParen, pos: start} case ':': p.pos++ return token{kind: tokHas, pos: start} case '-': p.pos++ return token{kind: tokMinus, pos: start} case '=': p.pos++ return token{kind: tokEq, pos: start} case '!': if p.pos+1 < len(p.input) && p.input[p.pos+1] == '=' { p.pos += 2 return token{kind: tokNe, pos: start} } p.pos++ return token{kind: tokText, value: "!", pos: start} case '<': if p.pos+1 < len(p.input) && p.input[p.pos+1] == '=' { p.pos += 2 return token{kind: tokLe, pos: start} } p.pos++ return token{kind: tokLt, pos: start} case '>': if p.pos+1 < len(p.input) && p.input[p.pos+1] == '=' { p.pos += 2 return token{kind: tokGe, pos: start} } p.pos++ return token{kind: tokGt, pos: start} case '"': return p.scanString(start) } return p.scanText(start) } func (p *parser) scanString(start int) token { p.pos++ // skip opening " var sb strings.Builder for p.pos < len(p.input) { ch := p.input[p.pos] if ch == '"' { p.pos++ break } if ch == '\\' && p.pos+1 < len(p.input) { p.pos++ sb.WriteByte(p.input[p.pos]) } else { sb.WriteByte(ch) } p.pos++ } return token{kind: tokString, value: sb.String(), pos: start} } func (p *parser) scanText(start int) token { for p.pos < len(p.input) { ch := p.input[p.pos] if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '(' || ch == ')' || ch == ':' || ch == '=' || ch == '!' || ch == '<' || ch == '>' || ch == '"' || ch == '-' { break } p.pos++ } value := p.input[start:p.pos] switch strings.ToUpper(value) { case "AND": return token{kind: tokAnd, value: value, pos: start} case "OR": return token{kind: tokOr, value: value, pos: start} case "NOT": return token{kind: tokNot, value: value, pos: start} } return token{kind: tokText, value: value, pos: start} } // expression = factor ((AND)? factor)* // AND between factors is optional; adjacent factors are implicitly AND'd. func (p *parser) parseExpression() (Expr, error) { left, err := p.parseFactor() if err != nil { return nil, err } for { tok := p.peek() if tok.kind == tokAnd { p.consume() } else if tok.kind != tokText && tok.kind != tokString && tok.kind != tokNot && tok.kind != tokMinus && tok.kind != tokLParen { break } right, err := p.parseFactor() if err != nil { return nil, err } left = AndExpr{Left: left, Right: right} } return left, nil } // factor = term (OR term)* func (p *parser) parseFactor() (Expr, error) { left, err := p.parseTerm() if err != nil { return nil, err } for p.peek().kind == tokOr { p.consume() right, err := p.parseTerm() if err != nil { return nil, err } left = OrExpr{Left: left, Right: right} } return left, nil } // term = (NOT | "-")? simple func (p *parser) parseTerm() (Expr, error) { tok := p.peek() if tok.kind == tokNot || tok.kind == tokMinus { p.consume() operand, err := p.parseSimple() if err != nil { return nil, err } return NotExpr{Operand: operand}, nil } return p.parseSimple() } // simple = restriction | "(" expression ")" func (p *parser) parseSimple() (Expr, error) { if p.peek().kind == tokLParen { p.consume() expr, err := p.parseExpression() if err != nil { return nil, err } if p.consume().kind != tokRParen { return nil, fmt.Errorf("expected closing parenthesis") } return expr, nil } return p.parseRestriction() } // restriction = field op value | bare_text func (p *parser) parseRestriction() (Expr, error) { tok := p.consume() // A quoted string with no field prefix is bare text. if tok.kind == tokString { return p.bareText(tok.value, tok.pos) } if tok.kind != tokText { return nil, fmt.Errorf("expected field name at position %d, got %q", tok.pos, tok.value) } field := tok.value next := p.peek() var op Op switch next.kind { case tokEq: p.consume() op = OpEq case tokNe: p.consume() op = OpNe case tokLt: p.consume() op = OpLt case tokGt: p.consume() op = OpGt case tokLe: p.consume() op = OpLe case tokGe: p.consume() op = OpGe case tokHas: p.consume() op = OpHas // field:(val1 val2 ...) expands to field:val1 OR field:val2 OR ... if p.peek().kind == tokLParen { p.consume() return p.parseValueList(field, op) } default: // No operator — treat the word itself as bare text. return p.bareText(field, tok.pos) } val := p.consume() if val.kind != tokText && val.kind != tokString { return nil, fmt.Errorf("expected value after operator at position %d", val.pos) } return CompareExpr{Field: field, Op: op, Value: val.value}, nil } // parseValueList parses "val1 val2 ...)" and expands to OR'd CompareExprs. func (p *parser) parseValueList(field string, op Op) (Expr, error) { var result Expr for { tok := p.peek() if tok.kind == tokRParen { p.consume() break } if tok.kind == tokEOF { return nil, fmt.Errorf("%w: unclosed value list for field %q", ErrInvalid, field) } val := p.consume() if val.kind != tokText && val.kind != tokString { return nil, fmt.Errorf("expected value at position %d, got %q", val.pos, val.value) } c := CompareExpr{Field: field, Op: op, Value: val.value} if result == nil { result = c } else { result = OrExpr{Left: result, Right: c} } } if result == nil { return nil, fmt.Errorf("%w: empty value list for field %q", ErrInvalid, field) } return result, nil } // bareText expands a bare word or quoted string using DefaultFields (OR'd, contains). func (p *parser) bareText(value string, _ int) (Expr, error) { if len(p.defaultFields) == 0 { return nil, fmt.Errorf("%w: %q has no operator; use field:value syntax", ErrInvalid, value) } var result Expr for _, f := range p.defaultFields { c := CompareExpr{Field: f, Op: OpHas, Value: value} if result == nil { result = c } else { result = OrExpr{Left: result, Right: c} } } return result, nil }