gamefic 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +1 -1
  4. data/lib/gamefic/active/epic.rb +1 -0
  5. data/lib/gamefic/active.rb +4 -0
  6. data/lib/gamefic/chapter.rb +25 -42
  7. data/lib/gamefic/command.rb +40 -1
  8. data/lib/gamefic/dispatcher.rb +5 -31
  9. data/lib/gamefic/entity.rb +26 -0
  10. data/lib/gamefic/narrative.rb +9 -5
  11. data/lib/gamefic/plot.rb +5 -0
  12. data/lib/gamefic/proxy.rb +46 -0
  13. data/lib/gamefic/query/base.rb +28 -12
  14. data/lib/gamefic/query/general.rb +3 -12
  15. data/lib/gamefic/query/result.rb +4 -1
  16. data/lib/gamefic/query/scoped.rb +1 -18
  17. data/lib/gamefic/query/text.rb +13 -12
  18. data/lib/gamefic/response.rb +65 -34
  19. data/lib/gamefic/scanner/base.rb +44 -0
  20. data/lib/gamefic/scanner/fuzzy.rb +17 -0
  21. data/lib/gamefic/scanner/fuzzy_nesting.rb +14 -0
  22. data/lib/gamefic/scanner/nesting.rb +39 -0
  23. data/lib/gamefic/scanner/result.rb +50 -0
  24. data/lib/gamefic/scanner/strict.rb +31 -0
  25. data/lib/gamefic/scanner.rb +33 -111
  26. data/lib/gamefic/scope/descendants.rb +16 -0
  27. data/lib/gamefic/scope/family.rb +31 -8
  28. data/lib/gamefic/scope.rb +1 -0
  29. data/lib/gamefic/scriptable/actions.rb +4 -23
  30. data/lib/gamefic/scriptable/entities.rb +4 -1
  31. data/lib/gamefic/scriptable/plot_proxies.rb +16 -0
  32. data/lib/gamefic/scriptable/proxies.rb +31 -0
  33. data/lib/gamefic/scriptable/queries.rb +15 -7
  34. data/lib/gamefic/scriptable/scenes.rb +7 -1
  35. data/lib/gamefic/scriptable.rb +67 -15
  36. data/lib/gamefic/stage.rb +2 -2
  37. data/lib/gamefic/subplot.rb +27 -1
  38. data/lib/gamefic/version.rb +1 -1
  39. data/lib/gamefic.rb +1 -1
  40. metadata +12 -4
  41. data/lib/gamefic/composer.rb +0 -70
  42. data/lib/gamefic/scriptable/proxy.rb +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79b61813b56cbbabb76ecbef9abb370b2fe77b58ea3e155377f8c4dfd2677684
4
- data.tar.gz: 611c108a0c080796c8308bee5f3e361c60fc4ddd316234bf9468c09742b1a52d
3
+ metadata.gz: 79309729d2296ceec16e10d3380fba331512f15ca2d303788c1fbcc83c077457
4
+ data.tar.gz: f4ce7d947730da4e833d895ae3085c4027d80cf89306b3aeff03d1a2a6ecd2aa
5
5
  SHA512:
6
- metadata.gz: 85732c76dae467bd5d032462ca23d34b03b7be59c6327202fd9bef307ef3d06bc40af085f32010cfae9dfd93a85b20524785313388964772534632cc95159625
7
- data.tar.gz: 5c3db85ff44b268bd7604d33e28b92ca454c82c2a812f023af39a3da816a4252dfea01869d313f1ab6d24a1f062782ac002f5bf8fc40fd435efaa0c1ac14d337
6
+ metadata.gz: 01c7c68bdc217447d90bda4c2bdd362e318c644dc3486999f6125d6c124193e0b33782a250272a2455ea98e2f531551f3819611c4139b191d3dccaa61d9e03d8
7
+ data.tar.gz: '06101965d9a47188f2b5b22e87f4f7b4bbeefd5f102d2405e1457c8a389a732e782cd4eedccec2176f2b237ad3783b1ab0058ce2721e451caa29e4e8a6d52cdc'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 3.5.0
2
+ - Configurable scanners
3
+ - Refactored scanners and queries
4
+ - Allow assignment to nil instance variables in stage
5
+ - Lazy proxies
6
+ - Remove buggy index proxies
7
+ - Chapter inherits Narrative
8
+ - Plot attribute proxy for chapters and subplots
9
+ - Entity#leave
10
+ - Descendants query
11
+ - Persistent subplots
12
+ - Entity#broadcast
13
+ - Ascendants in family query
14
+
1
15
  ## 3.4.0 - September 10, 2024
2
16
  - Chapters
3
17
  - Subplots and chapters do not repeat plot scripts
data/README.md CHANGED
@@ -16,7 +16,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
16
16
 
17
17
  ## Contributing
18
18
 
19
- Bug reports and pull requests are welcome on GitHub at https://github.com/castwide/gamefic-sdk.
19
+ Bug reports and pull requests are welcome on GitHub at https://github.com/castwide/gamefic.
20
20
 
21
21
  ## License
22
22
 
@@ -51,6 +51,7 @@ module Gamefic
51
51
  rulebooks.flat_map(&:syntaxes)
52
52
  end
53
53
 
54
+ # @return [Array<Response>]
54
55
  def responses_for(*verbs)
55
56
  rulebooks.to_a
56
57
  .reverse
@@ -212,6 +212,10 @@ module Gamefic
212
212
  false
213
213
  end
214
214
 
215
+ def acting?
216
+ !epic.empty?
217
+ end
218
+
215
219
  private
216
220
 
217
221
  # @return [Array<Dispatcher>]
@@ -6,17 +6,15 @@ module Gamefic
6
6
  # adding the required instance variables, methods, and attributes to the
7
7
  # plot.
8
8
  #
9
- # Chapters are similar to subplots with a few important exceptions:
10
- # * Chapters persist for the duration of the plot.
9
+ # Chapters are similar to subplots with three important exceptions:
10
+ # * Chapters normally persist for the duration of a plot.
11
11
  # * Players do not need to be introduced to a chapter.
12
- # * Scripts in chapters apply to the parent plot's rulebook.
13
- # * Using `make` to create an entity in a chapter adds it to the parent
14
- # plot's entity list.
12
+ # * Chapters share their plot's entities, players, and rulebook.
15
13
  #
16
14
  # @example
17
15
  # class MyChapter < Gamefic::Chapter
18
- # seed do
19
- # @thing = make Gamefic::Entity, name: 'chapter thing'
16
+ # def thing
17
+ # @thing ||= make Gamefic::Entity, name: 'chapter thing'
20
18
  # end
21
19
  # end
22
20
  #
@@ -25,64 +23,49 @@ module Gamefic
25
23
  # end
26
24
  #
27
25
  # plot = MyPlot.new
28
- # plot.entities #=> [<#Gamefic::Entity a chapter thing>]
29
- # plot.instance_exec { @thing } #=> nil
26
+ # plot.entities #=> [<#Gamefic::Entity a chapter thing>]
27
+ # plot.thing # raises NoMethodError
28
+ # plot.chapters.first.thing #=> <#Gamefic::Entity a chapter thing>
30
29
  #
31
- class Chapter
32
- extend Scriptable
33
-
34
- include Scriptable::Actions
35
- include Scriptable::Events
36
- include Scriptable::Proxy
37
- include Scriptable::Queries
38
- include Scriptable::Scenes
30
+ class Chapter < Narrative
31
+ extend Scriptable::PlotProxies
39
32
 
40
33
  # @return [Plot]
41
34
  attr_reader :plot
42
35
 
43
36
  # @param plot [Plot]
44
- def initialize plot
37
+ def initialize(plot)
45
38
  @plot = plot
46
- end
47
-
48
- def included_blocks
49
- self.class.included_blocks - plot.included_blocks
50
- end
51
-
52
- def seed
53
- included_blocks.select(&:seed?).each { |blk| Stage.run self, &blk.code }
39
+ # The plot is responsible for hydrating chapters
40
+ super(hydrate: false)
54
41
  end
55
42
 
56
43
  def script
57
44
  included_blocks.select(&:script?).each { |blk| Stage.run self, &blk.code }
58
45
  end
59
46
 
60
- def rulebook
61
- plot.rulebook
62
- end
63
-
64
- def make klass, **opts
65
- plot.make klass, **opts
47
+ def included_blocks
48
+ self.class.included_blocks - plot.included_blocks
66
49
  end
67
50
 
68
- def entities
69
- plot.entities
51
+ def rulebook
52
+ plot.rulebook
70
53
  end
71
54
 
72
- def players
73
- plot.players
55
+ def entity_vault
56
+ plot.entity_vault
74
57
  end
75
58
 
76
- def destroy entity
77
- plot.destroy entity
59
+ def player_vault
60
+ plot.player_vault
78
61
  end
79
62
 
80
- def pick description
81
- plot.pick description
63
+ def subplots
64
+ plot.subplots
82
65
  end
83
66
 
84
- def pick! description
85
- plot.pick! description
67
+ def branch(...)
68
+ plot.branch(...)
86
69
  end
87
70
  end
88
71
  end
@@ -10,11 +10,50 @@ module Gamefic
10
10
  # @return [Array<Array<Entity>, Entity, String>]
11
11
  attr_reader :arguments
12
12
 
13
+ # @return [Integer]
14
+ attr_reader :strictness
15
+
16
+ # @return [Integer]
17
+ attr_reader :precision
18
+
13
19
  # @param verb [Symbol]
14
20
  # @param arguments [Array<Array<Entity>, Entity, String>]
15
- def initialize verb, arguments
21
+ # @param strictness [Integer]
22
+ # @param precision [Integer]
23
+ #
24
+ # @todo Consider making strictness and precision required or providing
25
+ # another generator
26
+ def initialize verb, arguments, strictness = 0, precision = 0
16
27
  @verb = verb
17
28
  @arguments = arguments
29
+ @strictness = strictness
30
+ @precision = precision
31
+ end
32
+
33
+ class << self
34
+ # Compose a command from input.
35
+ #
36
+ # @param actor [Actor]
37
+ # @param input [String]
38
+ # @return [Command]
39
+ def compose actor, input
40
+ expressions = Syntax.tokenize(input, actor.epic.syntaxes)
41
+ expressions.flat_map { |expression| expression_to_commands(actor, expression) }
42
+ .first || Command.new(nil, [])
43
+ end
44
+
45
+ private
46
+
47
+ # @param actor [Actor]
48
+ # @param expression [Expression]
49
+ # @return [Array<Command>]
50
+ def expression_to_commands actor, expression
51
+ actor.epic
52
+ .responses_for(expression.verb)
53
+ .map { |response| response.to_command(actor, expression) }
54
+ .compact
55
+ .sort_by.with_index { |result, idx| [-result.precision, -result.strictness, idx] }
56
+ end
18
57
  end
19
58
  end
20
59
  end
@@ -10,7 +10,6 @@ module Gamefic
10
10
  @actor = actor
11
11
  @command = command
12
12
  @executed = false
13
- @finalized = false
14
13
  end
15
14
 
16
15
  # Run the dispatcher.
@@ -23,11 +22,11 @@ module Gamefic
23
22
  action = next_action
24
23
  return unless action
25
24
 
26
- run_before_action_hooks action
25
+ actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
27
26
  return if action.cancelled?
28
27
 
29
28
  action.execute
30
- run_after_action_hooks action
29
+ actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
31
30
  action
32
31
  end
33
32
 
@@ -46,9 +45,9 @@ module Gamefic
46
45
  # @param input [String]
47
46
  # @return [Dispatcher]
48
47
  def self.dispatch actor, input
49
- expressions = Syntax.tokenize(input, actor.epic.syntaxes)
50
- command = Composer.compose(actor, expressions)
51
- new(actor, command)
48
+ # expressions = Syntax.tokenize(input, actor.epic.syntaxes)
49
+ # new(actor, Command.compose(actor, expressions))
50
+ new(actor, Command.compose(actor, input))
52
51
  end
53
52
 
54
53
  # @param actor [Active]
@@ -82,31 +81,6 @@ module Gamefic
82
81
 
83
82
  return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
84
83
  end
85
- finalize
86
- end
87
-
88
- # @return [void]
89
- def run_before_action_hooks action
90
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
91
- end
92
-
93
- # @return [void]
94
- def run_after_action_hooks action
95
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
96
- end
97
-
98
- # If the dispatcher proceeds through all possible responses, it can fall
99
- # back to a nil response as a catchall for commands that could not be
100
- # completed.
101
- #
102
- # @return [Action, nil]
103
- def finalize
104
- return nil if @finalized
105
-
106
- @finalized = true
107
- @command = Command.new(nil, ["#{command.verb} #{command.arguments.join(' ').strip}"])
108
- @responses = actor.epic.responses_for(nil)
109
- next_action
110
84
  end
111
85
  end
112
86
  end
@@ -55,6 +55,32 @@ module Gamefic
55
55
  "#<#{self.class} #{name}>"
56
56
  end
57
57
 
58
+ # Move this entity to its parent entity.
59
+ #
60
+ # @example
61
+ # room = Gamefic::Entity.new(name: 'room')
62
+ # person = Gamefic::Entity.new(name: 'person', parent: room)
63
+ # thing = Gamefic::Entity.new(name: 'thing', parent: person)
64
+ #
65
+ # thing.parent #=> person
66
+ # thing.leave
67
+ # thing.parent #=> room
68
+ #
69
+ # @return [void]
70
+ def leave
71
+ self.parent = parent&.parent
72
+ end
73
+
74
+ # Tell a message to all of this entity's accessible descendants.
75
+ #
76
+ # @param message [String]
77
+ # @return [void]
78
+ def broadcast message
79
+ Query::Scoped.new(Scope::Descendants).select(self)
80
+ .that_are(Active, proc(&:acting?))
81
+ .each { |actor| actor.tell message }
82
+ end
83
+
58
84
  class << self
59
85
  # Set or update the default attributes for new instances.
60
86
  #
@@ -14,16 +14,18 @@ module Gamefic
14
14
  include Scriptable::Actions
15
15
  include Scriptable::Entities
16
16
  include Scriptable::Events
17
- include Scriptable::Proxy
17
+ include Scriptable::Proxies
18
18
  include Scriptable::Queries
19
19
  include Scriptable::Scenes
20
20
 
21
21
  attr_reader :rulebook
22
22
 
23
- def initialize
23
+ def initialize(hydrate: true)
24
+ return unless hydrate
25
+
24
26
  seed
25
27
  script
26
- post_initialize
28
+ post_script
27
29
  end
28
30
 
29
31
  def seed
@@ -35,15 +37,17 @@ module Gamefic
35
37
  included_blocks.select(&:script?).each { |blk| Stage.run self, &blk.code }
36
38
  end
37
39
 
40
+ # @return [Array<Module>]
38
41
  def included_blocks
39
42
  self.class.included_blocks
40
43
  end
41
44
 
42
- def post_initialize
45
+ def post_script
43
46
  entity_vault.lock
44
47
  rulebook.freeze
45
48
  end
46
49
 
50
+ # @return [Array<Symbol>]
47
51
  def scenes
48
52
  rulebook.scenes.names
49
53
  end
@@ -111,7 +115,7 @@ module Gamefic
111
115
 
112
116
  def hydrate
113
117
  script
114
- post_initialize
118
+ post_script
115
119
  end
116
120
 
117
121
  def self.inherited klass
data/lib/gamefic/plot.rb CHANGED
@@ -16,6 +16,11 @@ module Gamefic
16
16
  rulebook.scenes.with_defaults self
17
17
  end
18
18
 
19
+ def post_script
20
+ super
21
+ chapters.freeze
22
+ end
23
+
19
24
  def chapters
20
25
  @chapters ||= self.class.appended_chapters.map { |klass| klass.new(self) }
21
26
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Proxy
5
+ # @return [Symbol]
6
+ attr_reader :type
7
+
8
+ # @return [Symbol, Array<Symbol>, String, Integer]
9
+ attr_reader :key
10
+
11
+ # @param type [Symbol]
12
+ # @param key [Symbol, String]
13
+ def initialize type, key
14
+ @type = type
15
+ @key = key
16
+ validate_type
17
+ end
18
+
19
+ def fetch narrative
20
+ send(type, narrative) ||
21
+ raise(ArgumentError, "Unable to fetch entity from proxy agent symbol `#{key}`")
22
+ end
23
+
24
+ private
25
+
26
+ def attr narrative
27
+ Stage.run(narrative, [key].flatten) { |keys| keys.inject(self) { |obj, key| obj.send key } }
28
+ rescue NoMethodError
29
+ nil
30
+ end
31
+
32
+ def ivar narrative
33
+ narrative.instance_variable_get key
34
+ end
35
+
36
+ def pick narrative
37
+ narrative.pick! key
38
+ end
39
+
40
+ def validate_type
41
+ return if [:attr, :ivar, :pick].include?(type)
42
+
43
+ raise ArgumentError, "Invalid proxy type `#{type}` (must be :attr, :ivar, or :pick)"
44
+ end
45
+ end
46
+ end
@@ -13,6 +13,8 @@ module Gamefic
13
13
  # @return [Boolean]
14
14
  attr_reader :ambiguous
15
15
 
16
+ attr_accessor :narrative
17
+
16
18
  # @raise [ArgumentError] if any of the arguments are nil
17
19
  #
18
20
  # @param arguments [Array<Object>]
@@ -26,12 +28,6 @@ module Gamefic
26
28
 
27
29
  # Get a query result for a given subject and token.
28
30
  #
29
- # @note This method is retained as a convenience for authors. Narratives
30
- # should use Composer to build commands, as it provides more precise
31
- # matching of tokens to valid response arguments. Authors can use
32
- # #query to find entities that match a token regardless of whether the
33
- # result matches an available response.
34
- #
35
31
  # @example
36
32
  # respond :reds do |actor|
37
33
  # reds = available(ambiguous: true).query(actor, 'red').match
@@ -41,19 +37,28 @@ module Gamefic
41
37
  # @param subject [Gamefic::Entity]
42
38
  # @param token [String]
43
39
  # @return [Result]
44
- def query(_subject, _token)
45
- raise "#query not implemented for #{self.class}"
40
+ def query(subject, token)
41
+ scan = Scanner.scan(select(subject), token)
42
+ ambiguous? ? ambiguous_result(scan) : unambiguous_result(scan)
46
43
  end
44
+ alias filter query
47
45
 
48
46
  # Get an array of entities that match the query from the context of the
49
47
  # subject.
50
48
  #
49
+ # Subclasses should override this method.
50
+ #
51
51
  # @param subject [Entity]
52
52
  # @return [Array<Entity>]
53
53
  def select _subject
54
- raise "#select not implemented for #{self.class}"
54
+ []
55
55
  end
56
56
 
57
+ # True if the object is selectable by the subject.
58
+ #
59
+ # @param subject [Entity]
60
+ # @param object [Array<Entity>, Entity]
61
+ # @return [Boolean]
57
62
  def accept?(subject, object)
58
63
  available = select(subject)
59
64
  if ambiguous?
@@ -75,9 +80,9 @@ module Gamefic
75
80
  private
76
81
 
77
82
  def calculate_precision
78
- @arguments.sum(@ambiguous ? -1000 : 0) do |arg|
83
+ unproxied_arguments.sum(@ambiguous ? -1000 : 0) do |arg|
79
84
  case arg
80
- when Entity, Scriptable::Proxy::Agent
85
+ when Entity, Proxy
81
86
  1000
82
87
  when Class, Module
83
88
  class_depth(arg) * 100
@@ -107,7 +112,18 @@ module Gamefic
107
112
  def unambiguous_result scan
108
113
  return Result.new(nil, scan.token) unless scan.matched.one?
109
114
 
110
- Result.new(scan.matched.first, scan.remainder)
115
+ Result.new(scan.matched.first, scan.remainder, scan.strictness)
116
+ end
117
+
118
+ def unproxied_arguments
119
+ @unproxied_arguments ||= arguments.map do |arg|
120
+ case arg
121
+ when Proxy
122
+ arg.fetch(narrative)
123
+ else
124
+ arg
125
+ end
126
+ end
111
127
  end
112
128
  end
113
129
  end
@@ -20,16 +20,7 @@ module Gamefic
20
20
  end
21
21
 
22
22
  def select subject
23
- available_entities(subject).that_are(*@arguments)
24
- end
25
-
26
- def query subject, token
27
- filtered = available_entities(subject).that_are(*@arguments)
28
- return Result.new(token, nil) if filtered.include?(token)
29
-
30
- scan = Scanner.scan(filtered, token)
31
-
32
- ambiguous? ? ambiguous_result(scan) : unambiguous_result(scan)
23
+ available_entities(subject).that_are(*unproxied_arguments)
33
24
  end
34
25
 
35
26
  private
@@ -37,9 +28,9 @@ module Gamefic
37
28
  def available_entities(subject)
38
29
  if @entities.is_a?(Proc)
39
30
  if @entities.arity.zero?
40
- @entities.call
31
+ Stage.run narrative, &@entities
41
32
  else
42
- @entities.call(subject)
33
+ Stage.run narrative, subject, &@entities
43
34
  end
44
35
  else
45
36
  @entities
@@ -11,9 +11,12 @@ module Gamefic
11
11
  # @return [String]
12
12
  attr_reader :remainder
13
13
 
14
- def initialize match, remainder
14
+ attr_reader :strictness
15
+
16
+ def initialize match, remainder, strictness = 0
15
17
  @match = match
16
18
  @remainder = remainder
19
+ @strictness = strictness
17
20
  end
18
21
  end
19
22
  end
@@ -17,29 +17,12 @@ module Gamefic
17
17
 
18
18
  def select(subject)
19
19
  @scope.matches(subject)
20
- .that_are(*@arguments)
21
- end
22
-
23
- # @return [Result]
24
- def query(subject, token)
25
- available = @scope.matches(subject)
26
- .that_are(*@arguments)
27
- return Result.new(token, nil) if available.include?(token)
28
-
29
- scan = Scanner.scan(available, token)
30
-
31
- return ambiguous_result(scan) if ambiguous?
32
-
33
- unambiguous_result(scan)
20
+ .that_are(*unproxied_arguments)
34
21
  end
35
22
 
36
23
  def precision
37
24
  @precision ||= @scope.precision + calculate_precision
38
25
  end
39
-
40
- def ambiguous?
41
- @ambiguous
42
- end
43
26
  end
44
27
  end
45
28
  end
@@ -4,16 +4,20 @@ module Gamefic
4
4
  module Query
5
5
  # A special query that handles text instead of entities.
6
6
  #
7
- class Text
7
+ class Text < Base
8
8
  # @param argument [String, Regexp]
9
9
  def initialize argument = /.*/
10
- @argument = argument
10
+ super
11
11
  validate
12
12
  end
13
13
 
14
+ def argument
15
+ arguments.first
16
+ end
17
+
14
18
  # @return [String, Regexp]
15
19
  def select(_subject)
16
- @argument
20
+ argument
17
21
  end
18
22
 
19
23
  def query _subject, token
@@ -23,6 +27,7 @@ module Gamefic
23
27
  Result.new(nil, token)
24
28
  end
25
29
  end
30
+ alias filter query
26
31
 
27
32
  def precision
28
33
  0
@@ -32,25 +37,21 @@ module Gamefic
32
37
  match? argument
33
38
  end
34
39
 
35
- def ambiguous?
36
- true
37
- end
38
-
39
40
  private
40
41
 
41
42
  def match? token
42
- return false unless token.is_a?(String)
43
+ return false unless token.is_a?(String) && !token.empty?
43
44
 
44
- case @argument
45
+ case argument
45
46
  when Regexp
46
- token =~ @argument
47
+ token =~ argument
47
48
  else
48
- token == @argument
49
+ token == argument
49
50
  end
50
51
  end
51
52
 
52
53
  def validate
53
- return if @argument.is_a?(String) || @argument.is_a?(Regexp)
54
+ return if argument.is_a?(String) || argument.is_a?(Regexp)
54
55
 
55
56
  raise ArgumentError, 'Invalid text query argument'
56
57
  end