// Package filter: filter expression parsing, schema validation, and SQL compilation. package filter import ( "errors" "fmt" "strings" ) // ErrInvalid is returned for parse and validation errors. // Callers may wrap it for additional context. var ErrInvalid = errors.New("invalid filter") // ---- IR types ---- // Expr is a filter expression node. type Expr interface{ expr() } // AndExpr is a conjunction of two expressions. type AndExpr struct{ Left, Right Expr } // OrExpr is a disjunction of two expressions. type OrExpr struct{ Left, Right Expr } // NotExpr negates an expression. type NotExpr struct{ Operand Expr } // CompareExpr compares a named field to a literal value. type CompareExpr struct { Field string Op Op Value string } func (AndExpr) expr() {} func (OrExpr) expr() {} func (NotExpr) expr() {} func (CompareExpr) expr() {} // Op is a comparison operator. type Op int const ( OpEq Op = iota // = OpNe // != OpLt // < OpGt // > OpLe // <= OpGe // >= OpHas // : → LIKE %val% in SQL ) // ---- Schema types ---- // FieldType is the data type of a filterable field. type FieldType int const ( TypeString FieldType = iota TypeEnum // value must be one of Field.Values ) func (t FieldType) MarshalJSON() ([]byte, error) { switch t { case TypeString: return []byte(`"string"`), nil case TypeEnum: return []byte(`"enum"`), nil } return nil, fmt.Errorf("unknown FieldType %d", t) } // Field declares a single filterable field in a Schema. type Field struct { Name string // name used in filter expressions Type FieldType Values []string // allowed values for TypeEnum } // Schema declares the filterable fields for a resource type. type Schema struct { Fields []Field // DefaultFields are searched when a bare word or quoted string appears // with no field name or operator. Each match uses the : (contains) operator // and multiple fields are OR'd together. DefaultFields []string } // Valuer is implemented by enum types to expose their valid string values. // The zero/unknown value is excluded. type Valuer interface { FilterValues() []string } func (s Schema) String(name string) Schema { s.Fields = append(s.Fields, Field{Name: name, Type: TypeString}) return s } func (s Schema) Enum(name string, v Valuer) Schema { s.Fields = append(s.Fields, Field{Name: name, Type: TypeEnum, Values: v.FilterValues()}) return s } func (s Schema) Default(name string) Schema { s.DefaultFields = append(s.DefaultFields, name) return s } func (s Schema) field(name string) (Field, bool) { for _, f := range s.Fields { if f.Name == name { return f, true } } return Field{}, false } // ParseAndValidate parses a filter string and validates it against the schema. // Returns (nil, nil) for an empty filter string. // Bare words and quoted strings without a field operator are expanded using // Schema.DefaultFields (each using the : operator, OR'd together). // Enum values are matched case-insensitively and normalized to their canonical form. func (s Schema) ParseAndValidate(input string) (Expr, error) { expr, err := parseWith(input, s.DefaultFields) if err != nil { return nil, err } return s.normalize(expr) } // Validate checks that all field references in expr exist in the schema // and that enum values are in the allowed set. Returns nil if expr is nil. func (s Schema) Validate(expr Expr) error { return s.validate(expr) } // normalize validates the expression and returns a new expression with enum values // replaced by their canonical form (case-insensitive match against Field.Values). func (s Schema) normalize(expr Expr) (Expr, error) { if expr == nil { return nil, nil } switch e := expr.(type) { case AndExpr: left, err := s.normalize(e.Left) if err != nil { return nil, err } right, err := s.normalize(e.Right) if err != nil { return nil, err } return AndExpr{left, right}, nil case OrExpr: left, err := s.normalize(e.Left) if err != nil { return nil, err } right, err := s.normalize(e.Right) if err != nil { return nil, err } return OrExpr{left, right}, nil case NotExpr: operand, err := s.normalize(e.Operand) if err != nil { return nil, err } return NotExpr{operand}, nil case CompareExpr: f, ok := s.field(e.Field) if !ok { return nil, fmt.Errorf("%w: unknown field %q", ErrInvalid, e.Field) } if f.Type == TypeEnum { for _, v := range f.Values { if strings.EqualFold(v, e.Value) { e.Value = v return e, nil } } return nil, fmt.Errorf("%w: invalid value %q for field %q; must be one of: %s", ErrInvalid, e.Value, e.Field, strings.Join(f.Values, ", ")) } return e, nil } return expr, nil } func (s Schema) validate(expr Expr) error { if expr == nil { return nil } switch e := expr.(type) { case AndExpr: if err := s.validate(e.Left); err != nil { return err } return s.validate(e.Right) case OrExpr: if err := s.validate(e.Left); err != nil { return err } return s.validate(e.Right) case NotExpr: return s.validate(e.Operand) case CompareExpr: f, ok := s.field(e.Field) if !ok { return fmt.Errorf("%w: unknown field %q", ErrInvalid, e.Field) } if f.Type == TypeEnum { for _, v := range f.Values { if v == e.Value { return nil } } return fmt.Errorf("%w: invalid value %q for field %q; must be one of: %s", ErrInvalid, e.Value, e.Field, strings.Join(f.Values, ", ")) } } return nil }