Merge pull request #2828 from Elv13/matcher_v2

Improve `gears.matcher` to be more flexible.
This commit is contained in:
Emmanuel Lepage Vallée 2019-08-11 22:29:35 -07:00 committed by GitHub
commit 1e59fc7fd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 432 additions and 25 deletions

View File

@ -2,8 +2,6 @@
--- A table which content will be used to set the target object properties. --- A table which content will be used to set the target object properties.
-- --
-- foo
--
-- @rulecomponent properties -- @rulecomponent properties
-- @param table -- @param table
-- @see callbacks -- @see callbacks
@ -44,3 +42,22 @@
-- @param table -- @param table
-- @see rule -- @see rule
-- @see except -- @see except
--- Matches when one of every "category" of components match.
--
-- @rulecomponent rule_every
-- @param table
-- @see rule
-- @see except
--- An identifier for this rule.
--
-- It can be anything. It will be compared with the `==` operator. Strings are
-- highly recommended.
--
-- Setting an `id` is useful to be able to remove the rule by using its id
-- instead of a table reference. Modules can also listen to `rule::appended` and
-- modify or disable a rule.
--
-- @rulecomponent id
-- @param table|string|number|function

View File

@ -25,6 +25,10 @@
-- --
-- @DOC_text_gears_matcher_default_EXAMPLE@ -- @DOC_text_gears_matcher_default_EXAMPLE@
-- --
-- This example shows the different matching sections:
--
-- @DOC_text_gears_matcher_types_EXAMPLE@
--
-- More examples are available in `awful.rules`. -- More examples are available in `awful.rules`.
-- --
-- @author Julien Danjou <julien@danjou.info> -- @author Julien Danjou <julien@danjou.info>
@ -35,62 +39,173 @@
local gtable = require("gears.table") local gtable = require("gears.table")
local gsort = require("gears.sort") local gsort = require("gears.sort")
local gdebug = require("gears.debug") local gdebug = require("gears.debug")
local gobject = require("gears.object")
local protected_call = require("gears.protected_call") local protected_call = require("gears.protected_call")
local matcher = {} local matcher = {}
--- A rule has been added to a set of matching rules.
-- @signal rule::appended
-- @tparam table gears.matcher The matcher.
-- @tparam table rule The rule.
-- @tparam table source The matching rules source name.
-- @tparam table content The matching rules source content.
-- @see append_rule
-- @see append_rules
--- A rule has been removed to a set of matching rules.
-- @signal rule::removed
-- @tparam table gears.matcher The matcher.
-- @tparam table rule The rule.
-- @tparam table source The matching rules source name.
-- @tparam table content The matching rules source content.
-- @see remove_rule
--- A matching source function has been added.
-- @signal matching_function::added
-- @tparam table gears.matcher The matcher.
-- @tparam function callback The callback.
-- @see add_matching_function
--- A matching source table has been added.
-- @signal matching_rules::added
-- @tparam table gears.matcher The matcher.
-- @tparam function callback The callback.
-- @see add_matching_rules
--- A matching source function has been removed.
-- @signal matching_source::removed
-- @tparam table gears.matcher The matcher.
-- @see remove_matching_source
local function default_matcher(a, b)
return a == b or (type(a) == "string" and a:match(b))
end
local function greater_matcher(a, b)
return a > b
end
local function lesser_matcher(a, b)
return a < b
end
-- Check if an object matches a rule. -- Check if an object matches a rule.
-- @param o The object. -- @param o The object.
-- #tparam table rule The rule to check. -- #tparam table rule The rule to check.
-- @treturn boolean True if it matches, false otherwise. -- @treturn boolean True if it matches, false otherwise.
function matcher:_match(o, rule) -- @method _match
function matcher:_match(o, rule, matcher_f)
if not rule then return false end if not rule then return false end
matcher_f = matcher_f or default_matcher
for field, value in pairs(rule) do for field, value in pairs(rule) do
if o[field] then local pm = self._private.prop_matchers[field]
if type(o[field]) == "string" then if pm and pm(o, value, field) then
if not o[field]:match(value) and o[field] ~= value then return true
return false elseif not matcher_f(o[field], value) then
end
elseif o[field] ~= value then
return false
end
else
return false return false
end end
end end
return true return true
end end
local function field_matcher(self, o, field, value, matcher_f)
matcher_f = matcher_f or default_matcher
local pm = self._private.prop_matchers[field]
if pm and pm(o, value, field) then
return true
elseif matcher_f(o[field] , value) then
return true
end
return false
end
-- Check if an object matches any part of a rule. -- Check if an object matches any part of a rule.
-- @param o The object. -- @param o The object.
-- #tparam table rule The rule to check. -- #tparam table rule The rule _match_anyto check.
-- @treturn boolean True if at least one rule is matched, false otherwise. -- @treturn boolean True if at least one rule is matched, false otherwise.
-- @method _match_any
function matcher:_match_any(o, rule) function matcher:_match_any(o, rule)
if not rule then return false end if not rule then return false end
for field, values in pairs(rule) do for field, values in pairs(rule) do
if o[field] then if o[field] then
-- Special case, "all"
if type(values) == "boolean" and values then
return true
end
for _, value in ipairs(values) do for _, value in ipairs(values) do
if o[field] == value then if field_matcher(self, o, field, value) then
return true
elseif type(o[field]) == "string" and o[field]:match(value) then
return true return true
end end
end end
end end
end end
return false return false
end end
-- Check if an object matches at least one of every part of a rule.
--
-- @param o The object.
-- @tparam table rule The rule _match_anyto check.
-- @tparam boolean multi If the entries are table of choices.
-- @treturn boolean True if all rules are matched.
-- @method _match_every
function matcher:_match_every(o, rule)
if not rule then return true end
for field, values in pairs(rule) do
local found = false
for _, value in ipairs(values) do
if not field_matcher(self, o, field, value) then
found = true
break
end
end
if not found then
return false
end
end
return true
end
--- Does a given rule entry match an object? --- Does a given rule entry match an object?
-- @param o The object. -- @param o The object.
-- @tparam table entry Rule entry (with keys `rule`, `rule_any`, `except` and/or -- @tparam table entry Rule entry (with keys `rule`, `rule_any`, `except` and/or
-- `except_any`). -- `except_any`).
-- @treturn boolean If `o` matches `entry`. -- @treturn boolean If `o` matches `entry`.
-- @method matches_rule
function matcher:matches_rule(o, entry) function matcher:matches_rule(o, entry)
local match = self:_match(o, entry.rule) or self:_match_any(o, entry.rule_any) local match = self:_match(o, entry.rule) or self:_match_any(o, entry.rule_any)
return match
and (not self:_match(o, entry.except)) -- If there was `rule` or `rule_any` and they failed to match, look no further.
and (not self:_match_any(o, entry.except_any)) if (not match) and (entry.rule or entry.rule_any) then return false end
if not self:_match_every(o, entry.rule_every) then return false end
-- Negative matching.
if entry.except and self:_match(o, entry.except) then return false end
if entry.except_any and self:_match_any(o, entry.except_any) then return false end
-- Other operators.
if entry.rule_greater and not self:_match(o, entry.rule_greater, greater_matcher) then
return false
end
if entry.rule_lesser and not self:_match(o, entry.rule_lesser, lesser_matcher) then
return false
end
return true
end end
--- Get list of matching rules for an object. --- Get list of matching rules for an object.
@ -100,10 +215,29 @@ end
-- --
-- @param o The object. -- @param o The object.
-- @tparam[opt=nil] table rules The rules to check. List with "rule", "rule_any", -- @tparam[opt=nil] table rules The rules to check. List with "rule", "rule_any",
-- "except" and "except_any" keys. -- "except" and "except_any" keys. If no rules are provided, all rules
-- @treturn table The list of matched rules. -- registered with a source will be matched.
-- @method matching_rules
function matcher:matching_rules(o, rules) function matcher:matching_rules(o, rules)
-- Match all sources.
if not rules then
local ret = {}
for _, r in pairs(self._matching_rules) do
gtable.merge(ret, self:matching_rules(o, r))
end
return ret
end
local result = {} local result = {}
if not rules then
gdebug.print_warning("This matcher has no rule source")
return result
end
for _, entry in ipairs(rules) do for _, entry in ipairs(rules) do
if self:matches_rule(o, entry) then if self:matches_rule(o, entry) then
table.insert(result, entry) table.insert(result, entry)
@ -117,6 +251,7 @@ end
-- @tparam table rules The rules to check. List of tables with `rule`, -- @tparam table rules The rules to check. List of tables with `rule`,
-- `rule_any`, `except` and `except_any` keys. -- `rule_any`, `except` and `except_any` keys.
-- @treturn boolean True if at least one rule is matched, false otherwise. -- @treturn boolean True if at least one rule is matched, false otherwise.
-- @method matches_rules
function matcher:matches_rules(o, rules) function matcher:matches_rules(o, rules)
for _, entry in ipairs(rules) do for _, entry in ipairs(rules) do
if self:matches_rule(o, entry) then if self:matches_rule(o, entry) then
@ -126,6 +261,61 @@ function matcher:matches_rules(o, rules)
return false return false
end end
--- Assign a function to match an object property against a value.
--
-- The default matcher uses the `==` operator for all types. It also uses the
-- `:match()` method for string and allows pattern matching. If the value is a
-- function, then that function is called with the object and the current
-- properties to be applied. If the function returns true, the match is
-- accepted.
--
-- Custom property matcher are useful when objects are compared. This avoids
-- having to implement custom metatable for everything.
--
-- The `f` function receives 3 arguments:
--
-- * The object to match against (anything)
-- * The value to compare
-- * The property/field name.
--
-- It should return `true` if it matches and `false` otherwise.
--
-- @tparam string name The property name.
-- @tparam function f The matching function.
-- @method add_property_matcher
-- @usage -- Manually match the screen in various ways.
-- matcher:add_property_matcher("screen", function(c, value)
-- return c.screen == value
-- or screen[c.screen] == value
-- or c.screen.outputs[value] ~= nil
-- or value == "any"
-- or (value == "primary" and c.screen == screen.primary)
-- end)
--
function matcher:add_property_matcher(name, f)
assert(not self._private.prop_matchers[name], name .. " already has a matcher")
self._private.prop_matchers[name] = f
self:emit_signal("property_matcher::added", name, f)
end
--- Add a special setter for a property.
--
-- This is useful to add more properties to object which only make sense within
-- the context of a rule.
--
-- @method add_property_setter
-- @tparam string name The property name.
-- @tparam function f The setter function.
function matcher:add_property_setter(name, f)
assert(not self._private.prop_setters[name], name .. " already has a matcher")
self._private.prop_setters[name] = f
self:emit_signal("property_setter::added", name, f)
end
local function default_rules_callback(self, o, props, callbacks, rules) local function default_rules_callback(self, o, props, callbacks, rules)
for _, entry in ipairs(self:matching_rules(o, rules)) do for _, entry in ipairs(self:matching_rules(o, rules)) do
gtable.crush(props, entry.properties or {}) gtable.crush(props, entry.properties or {})
@ -146,6 +336,7 @@ end
-- @tparam[opt={}] table precede A list of names of sources this source has a -- @tparam[opt={}] table precede A list of names of sources this source has a
-- priority over. -- priority over.
-- @treturn boolean Returns false if a dependency conflict was found. -- @treturn boolean Returns false if a dependency conflict was found.
-- @method add_matching_rules
function matcher:add_matching_rules(name, rules, depends_on, precede) function matcher:add_matching_rules(name, rules, depends_on, precede)
local function matching_fct(_self, c, props, callbacks) local function matching_fct(_self, c, props, callbacks)
default_rules_callback(_self, c, props, callbacks, rules) default_rules_callback(_self, c, props, callbacks, rules)
@ -153,6 +344,8 @@ function matcher:add_matching_rules(name, rules, depends_on, precede)
self._matching_rules[name] = rules self._matching_rules[name] = rules
self:emit_signal("matching_rules::added", rules)
return self:add_matching_function(name, matching_fct, depends_on, precede) return self:add_matching_function(name, matching_fct, depends_on, precede)
end end
@ -171,6 +364,7 @@ end
-- @tparam[opt={}] table precede A list of names of sources this source has a -- @tparam[opt={}] table precede A list of names of sources this source has a
-- priority over. -- priority over.
-- @treturn boolean Returns false if a dependency conflict was found. -- @treturn boolean Returns false if a dependency conflict was found.
-- @method add_matching_function
function matcher:add_matching_function(name, callback, depends_on, precede) function matcher:add_matching_function(name, callback, depends_on, precede)
depends_on = depends_on or {} depends_on = depends_on or {}
precede = precede or {} precede = precede or {}
@ -221,6 +415,8 @@ function matcher:add_matching_function(name, callback, depends_on, precede)
end end
end end
self:emit_signal("matching_function::added", callback)
return true return true
end end
@ -231,16 +427,19 @@ end
-- --
-- @tparam string name The source name. -- @tparam string name The source name.
-- @treturn boolean If the source has been removed. -- @treturn boolean If the source has been removed.
-- @method remove_matching_source
function matcher:remove_matching_source(name) function matcher:remove_matching_source(name)
self._rule_source_sort:remove(name) self._rule_source_sort:remove(name)
for k, v in ipairs(self._matching_source) do for k, v in ipairs(self._matching_source) do
if v.name == name then if v.name == name then
self:emit_signal("matching_source::removed", v)
table.remove(self._matching_source, k) table.remove(self._matching_source, k)
return true return true
end end
end end
self._matching_rules[name] = nil self._matching_rules[name] = nil
return false return false
@ -252,6 +451,7 @@ end
-- and rules. -- and rules.
-- --
-- @param o The object. -- @param o The object.
-- @method apply
function matcher:apply(o) function matcher:apply(o)
local callbacks, props = {}, {} local callbacks, props = {}, {}
for _, v in ipairs(self._matching_source) do for _, v in ipairs(self._matching_source) do
@ -280,7 +480,9 @@ function matcher:_execute(o, props, callbacks)
value = value(o, props) value = value(o, props)
end end
if type(o[property]) == "function" then if self._private.prop_setters[property] then
self._private.prop_setters[property](o, value)
elseif type(o[property]) == "function" then
o[property](o, value) o[property](o, value)
else else
o[property] = value o[property] = value
@ -288,14 +490,59 @@ function matcher:_execute(o, props, callbacks)
end end
end end
--- Add a new rule to the default set.
-- @tparam string source The source name.
-- @tparam table rule A valid rule.
-- @method append_rule
function matcher:append_rule(source, rule)
if not self._matching_rules[source] then
self:add_matching_rules(source, {}, {}, {})
end
table.insert(self._matching_rules[source], rule)
self:emit_signal("rule::appended", rule, source, self._matching_rules[source])
end
--- Add a new rules to the default set.
-- @tparam string source The source name.
-- @tparam table rules A table with rules.
-- @method append_rules
function matcher:append_rules(source, rules)
for _, rule in ipairs(rules) do
self:append_rule(source, rule)
end
end
--- Remove a new rule to the default set.
-- @tparam string source The source name.
-- @tparam string|table rule An existing rule or its `id`.
-- @treturn boolean If the rule was removed.
-- @method remove_rule
function matcher:remove_rule(source, rule)
if not self._matching_rules[source] then return end
for k, v in ipairs(self._matching_rules[source]) do
if v == rule or v.id == rule then
table.remove(self._matching_rules[source], k)
self:emit_signal("rule::removed", rule, source, self._matching_rules[source])
return true
end
end
return false
end
local module = {} local module = {}
--- Create a new rule solver object. --- Create a new rule solver object.
-- @function gears.matcher -- @constructorfct gears.matcher
-- @return A new rule solver object. -- @return A new rule solver object.
local function new() local function new()
local ret = {} local ret = gobject()
rawset(ret, "_private", {
rules = {}, prop_matchers = {}, prop_setters = {}
})
-- Contains the sources. -- Contains the sources.
-- The elements are ordered "first in, first executed". Thus, the higher the -- The elements are ordered "first in, first executed". Thus, the higher the

View File

@ -12,6 +12,7 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE
local rule1 = { local rule1 = {
rule = { rule = {
answer = 42, answer = 42,
everything = true,
}, },
properties = { properties = {
name = "baz", name = "baz",
@ -69,6 +70,41 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE
-- This will add the `rules` to this matcher. -- This will add the `rules` to this matcher.
matcher:add_matching_rules("second", rules, {"first"}, {}) matcher:add_matching_rules("second", rules, {"first"}, {})
--DOC_NEWLINE
-- Some properties cannot be checked with the `==` operator (like those
-- with multiple possible types). In that case, it is possible to define
-- special comparator function.
matcher:add_property_matcher("everything", function(obj, value)
return value and obj.answer == 42
end)
--DOC_NEWLINE
-- The same can be done for the property section.
matcher:add_property_setter("multiply_by", function(obj, value)
obj.answer = (obj.answer or 1) * value
end)
--DOC_NEWLINE
-- It is possible to append rules to existing (or new) sources.
matcher:append_rule( "second", {
id = "rule_with_id",
rule = {
has_elite = true,
},
properties = {
multiply_by = "1337",
},
})
--DOC_NEWLINE
-- Or remove them.
local rm3 = --DOC_HIDE
matcher:remove_rule("second", "rule_with_id")
assert(rm3) --DOC_HIDE
--DOC_NEWLINE --DOC_NEWLINE
-- Apply the properties to `o` -- Apply the properties to `o`
@ -76,5 +112,12 @@ local gears = {matcher = require("gears.matcher")} --DOC_HIDE
assert(o.is_everything) --DOC_HIDE assert(o.is_everything) --DOC_HIDE
assert(o.name == "baz") --DOC_HIDE assert(o.name == "baz") --DOC_HIDE
local rm1 = --DOC_HIDE
matcher:remove_matching_source("first") --DOC_HIDE matcher:remove_matching_source("first") --DOC_HIDE
assert(rm1) --DOC_HIDE
matcher:append_rules("second", {{},{},{}}) --DOC_HIDE
local rm2 = --DOC_HIDE
matcher:remove_matching_source("second") --DOC_HIDE matcher:remove_matching_source("second") --DOC_HIDE
assert(rm2) --DOC_HIDE

View File

@ -0,0 +1,100 @@
--DOC_HIDE --DOC_NO_USAGE
local gears = {matcher = require("gears.matcher")} --DOC_HIDE
local matcher = gears.matcher()
--DOC_NEWLINE
matcher:append_rule( "my.source", {
rule = {
my_any_rule = true,
},
rule_every = {
every1 = {1, 42},
every2 = {2, 42},
every3 = {3, 42},
},
except = {
except1 = 1,
},
properties = {
was_a_match = true,
},
})
--DOC_NEWLINE
local candidate1 = {
my_any_rule = true,
every1 = 1,
every2 = 2,
every3 = 3,
was_a_match = false,
}
--DOC_NEWLINE
local candidate2 = {
every2 = 2,
was_a_match = false,
}
--DOC_NEWLINE
local candidate3 = {
my_any_rule = true,
was_a_match = false,
every1 = 1,
every2 = 2,
every3 = 3,
except1 = 1,
}
--DOC_NEWLINE
matcher:apply(candidate1)
matcher:apply(candidate2)
matcher:apply(candidate3)
--DOC_NEWLINE
-- Only candidate1 fits all criteria.
assert(candidate1.was_a_match == true )
assert(candidate2.was_a_match == false)
assert(candidate3.was_a_match == false)
--DOC_NEWLINE
-- It is also possible to match number property by range.
matcher:append_rule( "my.source", {
rule_greater = {
value = 50,
},
rule_lesser = {
value = 100,
},
properties = {
was_a_match = true,
},
})
--DOC_NEWLINE
local candidate4 = { value = 40 , was_a_match = false }
local candidate5 = { value = 75 , was_a_match = false }
local candidate6 = { value = 101, was_a_match = false }
--DOC_NEWLINE
matcher:apply(candidate4)
matcher:apply(candidate5)
matcher:apply(candidate6)
--DOC_NEWLINE
-- Only candidate5 fits all criteria.
assert(candidate4.was_a_match == false)
assert(candidate5.was_a_match == true )
assert(candidate6.was_a_match == false)