Class: SpectatorSport::SearchQuery

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Model
Defined in:
app/models/spectator_sport/search_query.rb,
app/models/spectator_sport/search_query/lexer.rb,
app/models/spectator_sport/search_query/parser.rb,
app/models/spectator_sport/search_query/syntax_tree.rb

Overview

Parses a small Gmail-search-style query language for filtering recordings by label and by created/updated date, and converts the result into an ActiveRecord scope.

label:value               matches a keyless label with this value
label:key:value           matches a label with this key and value
label:key:*               matches any label with this key, any value
label:key:2*              matches any label with this key whose value starts with "2"
                        ("*" matches zero or more characters anywhere in the value)
-label:key:value          matches recordings WITHOUT this label
created:2026-01-31        matches recordings created on that day
created:>=2026-01-31      matches recordings created on or after that day
                        (also <, >, <=; same set of operators for updated:)

Terms juxtaposed with whitespace combine with implicit AND, same as the explicit "AND" keyword; the explicit "OR" keyword and "..." groups both combine terms with OR instead (e.g. label:a OR label:b, or equivalently label:key:v2); "(...)" groups terms with AND (mainly useful with a leading "-" to negate a whole group). AND binds tighter than OR, whether AND is implicit or explicit. For example:

(label:priority:high {label:status:active label:status:pending}) label:owner:*
-label:billing:paid -(label:team:marketing label:project:beta)

Usage:

search_query = SpectatorSport::SearchQuery.new(query: params[:query])
if search_query.valid?
recordings = search_query.to_scope(base_scope: recordings)
else
search_query.errors[:query] # => ["Missing closing \")\""]
end

Defined Under Namespace

Modules: SyntaxTree Classes: Lexer, Parser, SyntaxError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#queryObject

Returns the value of attribute query.



39
40
41
# File 'app/models/spectator_sport/search_query.rb', line 39

def query
  @query
end

Class Method Details

.label_key_term(key) ⇒ Object

Builds the DSL term that matches any label with the given key, e.g. "label:env:*".



56
57
58
# File 'app/models/spectator_sport/search_query.rb', line 56

def self.label_key_term(key)
  "label:#{quote_segment(key)}:*"
end

.label_term(label) ⇒ Object

Builds the DSL term that matches a label, e.g. "label:env:prod" or "label:featured" for a keyless label. Segments containing characters the lexer treats as delimiters are wrapped in double quotes.



46
47
48
49
50
51
52
# File 'app/models/spectator_sport/search_query.rb', line 46

def self.label_term(label)
  if label.key.present?
    "label:#{quote_segment(label.key)}:#{quote_segment(label.value)}"
  else
    "label:#{quote_segment(label.value)}"
  end
end

.with_term(query, term) ⇒ Object

Returns query with term appended via the implicit-AND juxtaposition the parser uses, unless the exact term is already present.



62
63
64
65
66
67
68
# File 'app/models/spectator_sport/search_query.rb', line 62

def self.with_term(query, term)
  query = query.to_s.strip
  return term if query.empty?
  return query if query.split.include?(term)

  "#{query} #{term}"
end

Instance Method Details

#to_scope(base_scope: SpectatorSport::Recording.all) ⇒ Object



76
77
78
79
80
81
# File 'app/models/spectator_sport/search_query.rb', line 76

def to_scope(base_scope: SpectatorSport::Recording.all)
  return base_scope if query.blank?
  return base_scope.none unless valid?

  base_scope.where(id: parsed_query.to_scope.select(:id))
end