@@ -782,6 +782,82 @@ impl ResolvedEntityPathFilter {
782782 pub fn rules ( & self ) -> impl Iterator < Item = ( & ResolvedEntityPathRule , & RuleEffect ) > {
783783 self . rules . iter ( )
784784 }
785+
786+ /// Evaluate how a path matches against this filter.
787+ ///
788+ /// This returns detailed information about:
789+ /// - Whether any part of the subtree rooted at this path should be included
790+ /// - Whether this specific path matches the filter
791+ /// - Whether this specific path is explicitly included (not just via subtree)
792+ pub fn evaluate ( & self , path : & EntityPath ) -> FilterEvaluation {
793+ let mut subtree_included = false ;
794+ let mut matches_exactly = false ;
795+ let mut last_match: Option < ( RuleEffect , bool ) > = None ;
796+ let mut found_include_in_subtree = false ;
797+
798+ for ( rule, effect) in self . rules ( ) {
799+ if !found_include_in_subtree
800+ && * effect == RuleEffect :: Include
801+ && rule. resolved_path . starts_with ( path)
802+ {
803+ found_include_in_subtree = true ;
804+ subtree_included = true ;
805+ }
806+
807+ if !matches_exactly
808+ && * effect == RuleEffect :: Include
809+ && !rule. rule . include_subtree ( )
810+ && rule. resolved_path == * path
811+ {
812+ matches_exactly = true ;
813+ }
814+
815+ if rule. matches ( path) {
816+ last_match = Some ( ( * effect, rule. rule . include_subtree ( ) ) ) ;
817+ }
818+ }
819+
820+ if let Some ( ( effect, include_subtree) ) = last_match {
821+ match effect {
822+ RuleEffect :: Include => subtree_included = true ,
823+ RuleEffect :: Exclude => {
824+ if include_subtree && !found_include_in_subtree {
825+ // Entire subtree is excluded, and we've already checked that nothing
826+ // in the subtree was explicitly included.
827+ subtree_included = false ;
828+ }
829+ }
830+ }
831+ }
832+
833+ let matches = last_match. is_some_and ( |( effect, _) | effect == RuleEffect :: Include ) ;
834+
835+ FilterEvaluation {
836+ subtree_included,
837+ matches,
838+ matches_exactly,
839+ }
840+ }
841+ }
842+
843+ /// Result of evaluating a filter against an entity path.
844+ ///
845+ /// This provides detailed information about how a path matches a filter,
846+ /// which is useful for efficiently walking entity trees.
847+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
848+ pub struct FilterEvaluation {
849+ /// Whether any part of the subtree rooted at this path should be included.
850+ ///
851+ /// If `false`, the entire subtree can be skipped during tree traversal.
852+ pub subtree_included : bool ,
853+
854+ /// Whether this specific path matches the filter.
855+ pub matches : bool ,
856+
857+ /// Whether this specific path is explicitly included (not just via a subtree rule).
858+ ///
859+ /// This is `true` when there's an exact (non-subtree) inclusion rule for this path.
860+ pub matches_exactly : bool ,
785861}
786862
787863impl EntityPathRule {
@@ -1287,6 +1363,154 @@ mod tests {
12871363 ) ;
12881364 }
12891365
1366+ #[ test]
1367+ fn test_evaluate_filter ( ) {
1368+ let subst_env = EntityPathSubs :: empty ( ) ;
1369+
1370+ // Test with a simple include-all filter
1371+ let filter = EntityPathFilter :: parse_forgiving ( "+ /**" ) . resolve_forgiving ( & subst_env) ;
1372+
1373+ let eval = filter. evaluate ( & EntityPath :: from ( "/world" ) ) ;
1374+ assert ! ( eval. subtree_included, "/**should include /world subtree" ) ;
1375+ assert ! ( eval. matches, "/**should match /world" ) ;
1376+ assert ! ( !eval. matches_exactly, "/** doesn't exactly match /world" ) ;
1377+
1378+ // Test with complex filter rules
1379+ let filter = EntityPathFilter :: parse_forgiving (
1380+ r"
1381+ + /world/**
1382+ - /world/car/**
1383+ + /world/car/driver
1384+ " ,
1385+ )
1386+ . resolve_forgiving ( & subst_env) ;
1387+
1388+ // Test /world - should be included
1389+ let eval = filter. evaluate ( & EntityPath :: from ( "/world" ) ) ;
1390+ assert ! ( eval. subtree_included, "/world subtree should be included" ) ;
1391+ assert ! ( eval. matches, "/world should match" ) ;
1392+ assert ! (
1393+ !eval. matches_exactly,
1394+ "/world should not match exactly (matched by /world/** subtree rule)"
1395+ ) ;
1396+
1397+ // Test /world/house - should be included by /world/**
1398+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/house" ) ) ;
1399+ assert ! (
1400+ eval. subtree_included,
1401+ "/world/house subtree should be included"
1402+ ) ;
1403+ assert ! ( eval. matches, "/world/house should match" ) ;
1404+ assert ! (
1405+ !eval. matches_exactly,
1406+ "/world/house should not match exactly (matched by /world/** subtree rule)"
1407+ ) ;
1408+
1409+ // Test /world/car - should not match but subtree should be included (has exception deeper)
1410+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/car" ) ) ;
1411+ assert ! (
1412+ eval. subtree_included,
1413+ "/world/car subtree should be included (has /world/car/driver exception)"
1414+ ) ;
1415+ assert ! ( !eval. matches, "/world/car should not match" ) ;
1416+ assert ! (
1417+ !eval. matches_exactly,
1418+ "/world/car should not match exactly (excluded by - /world/car/**)"
1419+ ) ;
1420+
1421+ // Test /world/car/driver - should be included
1422+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/car/driver" ) ) ;
1423+ assert ! (
1424+ eval. subtree_included,
1425+ "/world/car/driver subtree should be included"
1426+ ) ;
1427+ assert ! ( eval. matches, "/world/car/driver should match" ) ;
1428+ assert ! (
1429+ eval. matches_exactly,
1430+ "/world/car/driver should match exactly (non-subtree rule)"
1431+ ) ;
1432+
1433+ // Test /world/car/hood - should be excluded
1434+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/car/hood" ) ) ;
1435+ assert ! (
1436+ !eval. subtree_included,
1437+ "/world/car/hood subtree should be excluded"
1438+ ) ;
1439+ assert ! ( !eval. matches, "/world/car/hood should not match" ) ;
1440+ assert ! (
1441+ !eval. matches_exactly,
1442+ "/world/car/hood should not match exactly (excluded)"
1443+ ) ;
1444+
1445+ // Test /other - should be excluded (no matching rule)
1446+ let eval = filter. evaluate ( & EntityPath :: from ( "/other" ) ) ;
1447+ assert ! ( !eval. subtree_included, "/other subtree should be excluded" ) ;
1448+ assert ! ( !eval. matches, "/other should not match" ) ;
1449+ assert ! (
1450+ !eval. matches_exactly,
1451+ "/other should not match exactly (no matching rule)"
1452+ ) ;
1453+
1454+ // Test exact match without subtree
1455+ let filter = EntityPathFilter :: parse_forgiving (
1456+ r"
1457+ + /world
1458+ + /world/car/driver
1459+ " ,
1460+ )
1461+ . resolve_forgiving ( & subst_env) ;
1462+
1463+ let eval = filter. evaluate ( & EntityPath :: from ( "/world" ) ) ;
1464+ assert ! ( eval. subtree_included, "/world should be included" ) ;
1465+ assert ! ( eval. matches, "/world should match" ) ;
1466+ assert ! (
1467+ eval. matches_exactly,
1468+ "/world should match exactly (non-subtree rule)"
1469+ ) ;
1470+
1471+ // Children should not be included (no subtree rule)
1472+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/house" ) ) ;
1473+ assert ! (
1474+ !eval. subtree_included,
1475+ "/world/house should not be included (parent has no subtree rule)"
1476+ ) ;
1477+ assert ! ( !eval. matches, "/world/house should not match" ) ;
1478+ assert ! (
1479+ !eval. matches_exactly,
1480+ "/world/house should not match exactly (no rule for this path)"
1481+ ) ;
1482+
1483+ // Test subtree_included when there's an include rule deeper in the tree
1484+ let filter = EntityPathFilter :: parse_forgiving (
1485+ r"
1486+ + /world/car/driver
1487+ " ,
1488+ )
1489+ . resolve_forgiving ( & subst_env) ;
1490+
1491+ let eval = filter. evaluate ( & EntityPath :: from ( "/world" ) ) ;
1492+ assert ! (
1493+ eval. subtree_included,
1494+ "/world should have subtree_included=true because /world/car/driver is included"
1495+ ) ;
1496+ assert ! ( !eval. matches, "/world should not match directly" ) ;
1497+ assert ! (
1498+ !eval. matches_exactly,
1499+ "/world should not match exactly (no rule for this path)"
1500+ ) ;
1501+
1502+ let eval = filter. evaluate ( & EntityPath :: from ( "/world/car" ) ) ;
1503+ assert ! (
1504+ eval. subtree_included,
1505+ "/world/car should have subtree_included=true because /world/car/driver is included"
1506+ ) ;
1507+ assert ! ( !eval. matches, "/world/car should not match directly" ) ;
1508+ assert ! (
1509+ !eval. matches_exactly,
1510+ "/world/car should not match exactly (no rule for this path)"
1511+ ) ;
1512+ }
1513+
12901514 #[ test]
12911515 fn test_unresolved ( ) {
12921516 // We should omit the properties from the unresolved filter.
0 commit comments