Class: Roby::Queries::TaskMatcher
- Inherits:
-
PlanObjectMatcher
- Object
- MatcherBase
- PlanObjectMatcher
- Roby::Queries::TaskMatcher
- Includes:
- DRoby::V5::Queries::TaskMatcherDumper
- Defined in:
- lib/roby/queries/task_matcher.rb,
lib/roby/droby/enable.rb
Overview
This class represents a predicate which can be used to filter tasks. To filter plan-related properties, use Query.
A TaskMatcher object is a AND combination of various tests against tasks.
For instance, if one does
matcher = TaskMatcher.new.which_fullfills(Tasks::Simple).pending
Then
matcher === task
will return true if task
is an instance of the Tasks::Simple model and is pending (not started yet), and false if one of these two characteristics is not true.
Direct Known Subclasses
Constant Summary collapse
- PLAN_PREDICATES =
{ mission_task?: :mission_tasks, permanent_task?: :permanent_tasks }.freeze
Instance Attribute Summary collapse
-
#arguments ⇒ Hash
readonly
Set of arguments that should be tested on the task.
-
#neg_plan_predicates ⇒ Object
readonly
private
Set of predicates specific to the plan (e.g. mission/permanent).
-
#plan_predicates ⇒ Object
readonly
private
Set of predicates specific to the plan (e.g. mission/permanent).
Attributes inherited from PlanObjectMatcher
#children, #indexed_neg_predicates, #indexed_predicates, #instance, #model, #owners, #parents, #scope
Attributes inherited from MatcherBase
Class Method Summary collapse
- .match_indexed_predicate(name, index: name.to_s, neg_index: nil, not_index: nil, not_neg_index: name.to_s) ⇒ Object private
- .match_indexed_predicates(*names) ⇒ Object
Instance Method Summary collapse
-
#&(other) ⇒ Object
Computes the intersection of two predicates.
-
#===(task) ⇒ Object
True if
task
matches all the criteria defined on this object. -
#abstract? ⇒ Object
:method: not_finishing.
- #add_indexed_neg_predicate(name) ⇒ Object
- #add_indexed_predicate(name) ⇒ Object
-
#add_neg_plan_predicate(predicate) ⇒ Object
private
Helper to add a plan predicate in the match set.
- #add_neg_predicate(name) ⇒ Object
-
#add_plan_predicate(predicate) ⇒ Object
private
Helper to add a plan predicate in the match set.
- #add_predicate(name) ⇒ Object
-
#each_in_plan(plan, &block) ⇒ Object
Enumerate the objects matching self in the plan.
-
#evaluate(plan) ⇒ Object
private
Resolves or returns the cached set of matching tasks in the plan.
-
#filter(initial_set, index, initial_is_complete: false) ⇒ Object
deprecated
Deprecated.
use #filter_tasks_sets instead
-
#filter_tasks_sets(initial_set, index, initial_is_complete: false) ⇒ ([Set],[Set])
Filters tasks from an initial set to remove as many not-matching tasks as possible.
- #find_event(event_name) ⇒ Object
-
#handle_parent_child_arguments(other_query, relation, relation_options) ⇒ Object
Helper method for #with_child and #with_parent.
-
#indexed_query? ⇒ Boolean
Returns true if filtering with this TaskMatcher using #=== is equivalent to calling #filter() using a Index.
-
#indexed_sets(index) ⇒ (Set,Set)
private
Resolve the indexed sets needed to filter an initial set in #filter.
-
#initialize ⇒ TaskMatcher
constructor
Initializes an empty TaskMatcher object.
- #method_missing(m, *args) ⇒ Object
-
#mission ⇒ Object
Matches if the task is a mission.
-
#negate ⇒ Object
Negates this predicate.
-
#not_mission ⇒ Object
Matches if the task is not a mission.
-
#not_permanent ⇒ Object
Matches if the task is not permanent.
-
#permanent ⇒ Object
Matches if the task is permanent.
- #respond_to_missing?(m, include_private) ⇒ Boolean
-
#roots(plan, in_relation) ⇒ #each_in_plan
Filters tasks which have no parents in the query itself.
- #to_s ⇒ Object
-
#which_fullfills(model, arguments = nil) ⇒ Object
Filters on task model and arguments.
-
#with_arguments(arguments) ⇒ Object
Filters on the arguments that are declared in the model.
-
#with_model_arguments(arguments) ⇒ Object
Filters on the arguments that are declared in the model.
-
#|(other) ⇒ Object
Computes the union of two predicates.
Methods included from DRoby::V5::Queries::TaskMatcherDumper
Methods inherited from PlanObjectMatcher
#executable?, #global_scope, #global_scope?, #handle_parent_child_match, #local_scope, #local_scope?, #matches_child_constraints?, #matches_parent_constraints?, #not_self_owned, #owned_by, #self_owned, #with_child, #with_instance, #with_model, #with_parent
Methods included from DRoby::V5::Queries::PlanObjectMatcherDumper
Methods inherited from MatcherBase
declare_class_methods, #describe_failed_match, #each, #match, match_predicate, match_predicates, #reset, #to_a, #to_set
Constructor Details
#initialize ⇒ TaskMatcher
Initializes an empty TaskMatcher object
43 44 45 46 47 48 49 |
# File 'lib/roby/queries/task_matcher.rb', line 43 def initialize super @arguments = {} @indexed_query = !@instance @plan_predicates = Set.new @neg_plan_predicates = Set.new end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(m, *args) ⇒ Object
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 |
# File 'lib/roby/queries/task_matcher.rb', line 543 def method_missing(m, *args) m_string = m.to_s return super unless m_string.end_with?("_event") event_name = m[0..-7] model = find_event(event_name) if !model task_models = @model.empty? ? [Roby::Task] : @model raise NoMethodError.new(m), "no event '#{event_name}' in match model "\ "#{task_models.map(&:to_s).join(', ')}, use "\ "#which_fullfills to narrow the task model" elsif !args.empty? raise ArgumentError, "#{m} expected zero arguments, got #{args.size}" end TaskEventGeneratorMatcher.new(self, event_name) end |
Instance Attribute Details
#arguments ⇒ Hash (readonly)
Set of arguments that should be tested on the task
25 26 27 |
# File 'lib/roby/queries/task_matcher.rb', line 25 def arguments @arguments end |
#neg_plan_predicates ⇒ Object (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Set of predicates specific to the plan (e.g. mission/permanent)
40 41 42 |
# File 'lib/roby/queries/task_matcher.rb', line 40 def neg_plan_predicates @neg_plan_predicates end |
#plan_predicates ⇒ Object (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Set of predicates specific to the plan (e.g. mission/permanent)
35 36 37 |
# File 'lib/roby/queries/task_matcher.rb', line 35 def plan_predicates @plan_predicates end |
Class Method Details
.match_indexed_predicate(name, index: name.to_s, neg_index: nil, not_index: nil, not_neg_index: name.to_s) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/roby/queries/task_matcher.rb', line 172 def match_indexed_predicate( name, index: name.to_s, neg_index: nil, not_index: nil, not_neg_index: name.to_s ) method_name = name.to_s.gsub(/\?$/, "") class_eval <<~PREDICATE_METHOD, __FILE__, __LINE__ + 1 def #{method_name} add_predicate(:#{name}) #{"add_indexed_predicate(:#{index})" if index} #{"add_indexed_neg_predicate(:#{neg_index})" if neg_index} self end def not_#{method_name} add_neg_predicate(:#{name}) #{"add_indexed_predicates(:#{not_index})" if not_index} #{"add_indexed_neg_predicate(:#{not_neg_index})" if not_neg_index} self end PREDICATE_METHOD declare_class_methods(method_name, "not_#{method_name}") end |
.match_indexed_predicates(*names) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/roby/queries/task_matcher.rb', line 195 def match_indexed_predicates(*names) names.each do |n| unless Index::PREDICATES.include?(n) raise ArgumentError, "#{n} is not declared in Index::PREDICATES. Use "\ "match_indexed_predicate directly to override "\ "this check" end match_indexed_predicate(n) end end |
Instance Method Details
#&(other) ⇒ Object
Computes the intersection of two predicates
The returned task matcher will yield tasks that are matched by all the elements.
Roby does only supports computing the AND of task matchers (cannot combine different operators)
630 631 632 633 634 635 636 637 638 639 640 641 642 |
# File 'lib/roby/queries/task_matcher.rb', line 630 def &(other) result = AndMatcher::Tasks.new result << self case other when AndMatcher::Tasks result.merge(other) when TaskMatcher result << other else raise ArgumentError, "cannot compute the intersection of a TaskMatcher with #{other}" end end |
#===(task) ⇒ Object
True if task
matches all the criteria defined on this object.
424 425 426 427 428 429 430 431 432 433 |
# File 'lib/roby/queries/task_matcher.rb', line 424 def ===(task) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity return unless task.kind_of?(Roby::Task) return unless task.arguments.slice(*arguments.keys) == arguments return unless super return unless (plan = task.plan) return unless @plan_predicates.all? { |pred| plan.send(pred, task) } return if @neg_plan_predicates.any? { |pred| plan.send(pred, task) } true end |
#abstract? ⇒ Object
:method: not_finishing
Matches if the task is not finishing
See also #finishing, Task#finishing?
335 336 337 338 |
# File 'lib/roby/queries/task_matcher.rb', line 335 match_predicates( :abstract?, :partially_instanciated?, :fully_instanciated?, :interruptible? ) |
#add_indexed_neg_predicate(name) ⇒ Object
164 165 166 167 168 |
# File 'lib/roby/queries/task_matcher.rb', line 164 def add_indexed_neg_predicate(name) return if @indexed_neg_predicates.include?(name) @indexed_neg_predicates << name end |
#add_indexed_predicate(name) ⇒ Object
160 161 162 |
# File 'lib/roby/queries/task_matcher.rb', line 160 def add_indexed_predicate(name) @indexed_predicates << name unless @indexed_predicates.include?(name) end |
#add_neg_plan_predicate(predicate) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Helper to add a plan predicate in the match set
369 370 371 372 373 374 375 376 377 378 |
# File 'lib/roby/queries/task_matcher.rb', line 369 def add_neg_plan_predicate(predicate) if !PLAN_PREDICATES.key?(predicate) raise ArgumentError, "unknown plan predicate #{predicate}" elsif @plan_predicates.include?(predicate) raise ArgumentError, "trying to match #{predicate} & not_#{predicate}" end @neg_plan_predicates << predicate self end |
#add_neg_predicate(name) ⇒ Object
154 155 156 157 158 |
# File 'lib/roby/queries/task_matcher.rb', line 154 def add_neg_predicate(name) @indexed_query = false unless Index::PREDICATES.include?(name) super end |
#add_plan_predicate(predicate) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Helper to add a plan predicate in the match set
355 356 357 358 359 360 361 362 363 364 |
# File 'lib/roby/queries/task_matcher.rb', line 355 def add_plan_predicate(predicate) if !PLAN_PREDICATES.key?(predicate) raise ArgumentError, "unknown plan predicate #{predicate}" elsif @neg_plan_predicates.include?(predicate) raise ArgumentError, "trying to match #{predicate} & not_#{predicate}" end @plan_predicates << predicate self end |
#add_predicate(name) ⇒ Object
148 149 150 151 152 |
# File 'lib/roby/queries/task_matcher.rb', line 148 def add_predicate(name) @indexed_query = false unless Index::PREDICATES.include?(name) super end |
#each_in_plan(plan, &block) ⇒ Object
Enumerate the objects matching self in the plan
This resolves the query only the first time. After the first call, the same set of tasks will be returned. Use MatcherBase#reset to clear the cached results.
585 586 587 588 589 |
# File 'lib/roby/queries/task_matcher.rb', line 585 def each_in_plan(plan, &block) return enum_for(__method__, plan) unless block_given? evaluate(plan).each_in_plan(plan, &block) end |
#evaluate(plan) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolves or returns the cached set of matching tasks in the plan
This is a cached value, so use MatcherBase#reset to actually recompute this set.
This should not be called directly. Use #each_in_plan
576 577 578 |
# File 'lib/roby/queries/task_matcher.rb', line 576 def evaluate(plan) plan.query_result_set(self) end |
#filter(initial_set, index, initial_is_complete: false) ⇒ Object
use #filter_tasks_sets instead
482 483 484 485 486 487 |
# File 'lib/roby/queries/task_matcher.rb', line 482 def filter(initial_set, index, initial_is_complete: false) Roby.warn_deprecated "TaskMatcher#filter is deprecated, "\ "use {#filter_tasks_sets} instead" filter_tasks_sets(initial_set, index, initial_is_complete: initial_is_complete) end |
#filter_tasks_sets(initial_set, index, initial_is_complete: false) ⇒ ([Set],[Set])
Filters tasks from an initial set to remove as many not-matching tasks as possible
If #indexed_query? is true, the result is required to be exact (i.e. return exactly all tasks in initial_set that match the query)
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 |
# File 'lib/roby/queries/task_matcher.rb', line 500 def filter_tasks_sets(initial_set, index, initial_is_complete: false) positive_sets, negative_sets = indexed_sets(index) if !initial_is_complete || positive_sets.empty? positive_sets << initial_set end negative = negative_sets.shift || Set.new unless negative_sets.empty? negative = negative.dup negative_sets.each { |set| negative.merge(set) } end positive_sets = positive_sets.sort_by(&:size) result = Set.new result.compare_by_identity positive_sets.shift.each do |obj| next if negative.include?(obj) result.add(obj) if positive_sets.all? { |set| set.include?(obj) } end result end |
#find_event(event_name) ⇒ Object
524 525 526 527 528 529 530 531 532 533 534 535 536 537 |
# File 'lib/roby/queries/task_matcher.rb', line 524 def find_event(event_name) event_name = event_name.to_sym models = if !@model.empty? @model else [Roby::Task] end models.each do |m| if m.find_event(event_name) return TaskEventGeneratorMatcher.new(self, event_name) end end nil end |
#handle_parent_child_arguments(other_query, relation, relation_options) ⇒ Object
Helper method for #with_child and #with_parent
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'lib/roby/queries/task_matcher.rb', line 405 def handle_parent_child_arguments(other_query, relation, ) if !other_query.kind_of?(TaskMatcher) && !other_query.kind_of?(Task) if relation.kind_of?(Hash) arguments = relation relation = arguments.delete(:relation) || arguments.delete("relation") = arguments.delete(:relation_options) || arguments.delete("relation_options") else arguments = {} end other_query = TaskMatcher.which_fullfills(other_query, arguments) end [relation, [other_query, ]] end |
#indexed_query? ⇒ Boolean
Returns true if filtering with this TaskMatcher using #=== is equivalent to calling #filter() using a Index. This is used to avoid an explicit O(N) filtering step after filter() has been called
438 439 440 |
# File 'lib/roby/queries/task_matcher.rb', line 438 def indexed_query? @indexed_query end |
#indexed_sets(index) ⇒ (Set,Set)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolve the indexed sets needed to filter an initial set in #filter
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 |
# File 'lib/roby/queries/task_matcher.rb', line 449 def indexed_sets(index) positive_sets = [] @model.each do |m| positive_sets << index.by_model[m] end @owners.each do |o| candidates = index.by_owner[o] return [Set.new, Set.new] unless candidates positive_sets << candidates end @indexed_predicates.each do |pred| positive_sets << index.by_predicate[pred] end negative_sets = @indexed_neg_predicates .map { |pred| index.by_predicate[pred] } @plan_predicates.each do |name| positive_sets << index.send(PLAN_PREDICATES.fetch(name)) end @neg_plan_predicates.each do |name| negative_sets << index.send(PLAN_PREDICATES.fetch(name)) end [positive_sets, negative_sets] end |
#mission ⇒ Object
Matches if the task is a mission
381 382 383 |
# File 'lib/roby/queries/task_matcher.rb', line 381 def mission add_plan_predicate :mission_task? end |
#negate ⇒ Object
Negates this predicate
The returned task matcher will yield tasks that are not matched by self
648 649 650 |
# File 'lib/roby/queries/task_matcher.rb', line 648 def negate NotMatcher::Tasks.new(self) end |
#not_mission ⇒ Object
Matches if the task is not a mission
386 387 388 |
# File 'lib/roby/queries/task_matcher.rb', line 386 def not_mission add_neg_plan_predicate :mission_task? end |
#not_permanent ⇒ Object
Matches if the task is not permanent
398 399 400 |
# File 'lib/roby/queries/task_matcher.rb', line 398 def not_permanent add_neg_plan_predicate :permanent_task? end |
#permanent ⇒ Object
Matches if the task is permanent
393 394 395 |
# File 'lib/roby/queries/task_matcher.rb', line 393 def permanent add_plan_predicate :permanent_task? end |
#respond_to_missing?(m, include_private) ⇒ Boolean
539 540 541 |
# File 'lib/roby/queries/task_matcher.rb', line 539 def respond_to_missing?(m, include_private) m.to_s.end_with?("_event") || super end |
#roots(plan, in_relation) ⇒ #each_in_plan
Filters tasks which have no parents in the query itself.
Will filter out tasks which have parents in relation
that are included in the query result.
597 598 599 600 |
# File 'lib/roby/queries/task_matcher.rb', line 597 def roots(plan, in_relation) evaluate(plan).roots(in_relation) self end |
#to_s ⇒ Object
51 52 53 54 55 56 57 58 |
# File 'lib/roby/queries/task_matcher.rb', line 51 def to_s result = super unless arguments.empty? args_to_s = arguments.map { |k, v| ":#{k} => #{v}" }.join(", ") result << ".with_arguments(#{args_to_s})" end result end |
#which_fullfills(model, arguments = nil) ⇒ Object
Filters on task model and arguments
Will match if the task is an instance of model
or one of its subclasses, and if parts of its arguments are the ones provided. Set arguments
to nil if you don’t want to filter on arguments.
65 66 67 68 69 |
# File 'lib/roby/queries/task_matcher.rb', line 65 def which_fullfills(model, arguments = nil) with_model(model) with_model_arguments(arguments) if arguments self end |
#with_arguments(arguments) ⇒ Object
Filters on the arguments that are declared in the model
Will match if the task arguments for which there is a value in arguments
are set to that very value. Unlike #with_model_arguments, all values set in arguments
are considered.
See also #with_model_arguments
Example:
class TaskModel < Roby::Task
argument :a
argument :b
end
task = TaskModel.new(a: 10, b: 20)
# Matches on :a, :b is ignored altogether
TaskMatcher.new.
with_arguments(a: 10) === task # => true
# Looks for both :a and :b
TaskMatcher.new.
with_arguments(a: 10, b: 30) === task # => false
# Looks for both :a and :c, even though :c is not declared in TaskModel
TaskMatcher.new.
with_arguments(a: 10, c: 30) === task # => false
135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/roby/queries/task_matcher.rb', line 135 def with_arguments(arguments) @arguments ||= {} @indexed_query = false self.arguments.merge!(arguments) do |k, old, new| if old != new raise ArgumentError, "a constraint has already been set on the #{k} argument" end old end self end |
#with_model_arguments(arguments) ⇒ Object
Filters on the arguments that are declared in the model
Will match if the task arguments for which there is a value in arguments
are set to that very value, only looking at arguments that are defined in the model set by #with_model.
See also #with_arguments
Example:
class TaskModel < Roby::Task
argument :a
argument :b
end
task = TaskModel.new(a: 10, b: 20)
# Matches on :a, :b is ignored altogether
TaskMatcher.new.
with_model(TaskModel).
with_model_arguments(a: 10) === task # => true
# Looks for both :a and :b
TaskMatcher.new.
with_model(TaskModel).
with_model_arguments(a: 10, b: 30) === task # => false
# Matches on :a, :c is ignored as it is not an argument of +TaskModel+
TaskMatcher.new.
with_model(TaskModel).
with_model_arguments(a: 10, c: 30) === task # => true
In general, one would use #which_fullfills, which sets both the model and the model arguments
102 103 104 105 106 107 108 |
# File 'lib/roby/queries/task_matcher.rb', line 102 def with_model_arguments(arguments) valid_arguments = model.inject([]) do |set, model| set | model.arguments.to_a end with_arguments(arguments.slice(*valid_arguments)) self end |
#|(other) ⇒ Object
Computes the union of two predicates
The returned task matcher will yield tasks that are matched by any of its the elements.
Roby does only supports computing the OR of task matchers (cannot combine different operators)
609 610 611 612 613 614 615 616 617 618 619 620 621 |
# File 'lib/roby/queries/task_matcher.rb', line 609 def |(other) result = OrMatcher::Tasks.new result << self case other when OrMatcher::Tasks result.merge(other) when TaskMatcher result << other else raise ArgumentError, "cannot compute the union of a TaskMatcher with #{other}" end end |