Class: RoundhouseUi::FilterQuery::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/roundhouse_ui/filter_query.rb

Overview

An index walk. No regex over the input, so there is nothing to backtrack.

Instance Method Summary collapse

Constructor Details

#initialize(str, keys = FilterQuery::KEYS) ⇒ Parser

Returns a new instance of Parser.



329
330
331
332
333
334
335
# File 'lib/roundhouse_ui/filter_query.rb', line 329

def initialize(str, keys = FilterQuery::KEYS)
  @s = str
  @keys = keys
  @i = 0
  @seen = {}
  @ignored = []
end

Instance Method Details

#callObject



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/roundhouse_ui/filter_query.rb', line 337

def call
  loop do
    skip_space
    break if @i >= @s.length

    key = read_key
    # An unrecognised key refuses the whole query. Falling through to free
    # text is the one resolution that can select MORE than was typed, and a
    # literal substring search for "clas=Foo" would find nothing and read as
    # "no such jobs exist".
    return refuse(nil) if key == :unknown
    break if key.nil?

    value = read_value
    return refuse("Unclosed quote in #{key}=. Quote both ends, or drop the quotes.") if value == :unterminated
    return refuse(bad_value_message(key, value)) unless value_ok?(value)
    # A tag is a key:value pair or it is nothing. Rather than refuse the whole
    # query, the facet is DROPPED and recorded — a typo should not stop you
    # browsing. What it does stop is a bulk action: `tag=garbage queue=default`
    # drops to `queue=default`, which selects the whole queue, and the Delete
    # beneath it would then be scoped wider than what was typed. Dropping a
    # facet can only ever widen, so a degraded query authorises nothing.
    # Both halves must be non-empty. `tag=squad:` has a colon, so a
    # colon-only check let it through: the pill rendered, tag_pair came back nil,
    # and entry_selected?'s `return true if tag.nil?` selected everything — with
    # bulk authorised, because any_facets? was true.
    if key == "tag" && value.split(":", 2).reject(&:empty?).size != 2
      @ignored << [ "#{key}=#{value}", "tag= takes key:value, like tag=squad:core" ]
      next
    end

    if @seen.key?(key) && @seen[key] != value
      return refuse(%(Two different values for #{key}: "#{@seen[key]}" and "#{value}". Pick one.))
    end

    @seen[key] = value
  end

  residue = @s[@i..].to_s.strip

  # text= is the escape hatch for facet-shaped free text. Having BOTH is two
  # ways of saying the same thing, and picking one would discard the other
  # silently — `text=foo bar` would have kept "foo" and thrown "bar" away.
  if @seen.key?("text") && !residue.empty?
    return refuse(%(Text is given twice: text="#{@seen['text']}" and "#{residue}". ) +
                  %(Quote the whole thing: text="#{@seen['text']} #{residue}"))
  end

  unsupported = unsupported_in(residue)
  return refuse(unsupported) if unsupported

  FilterQuery.new(klass: @seen["class"], error: @seen["error"], queue: @seen["queue"],
                  tag: @seen["tag"], text: @seen.fetch("text", residue), raw: @s,
                  ignored: @ignored)
end