Skip to main content

Crate smix_selector

Crate smix_selector 

Source
Expand description

§smix-selector

Crates.io docs.rs License

Structured Selector enum + match_text for iOS-Simulator a11y trees. Six mutually-exclusive base forms (text / id / label / role+name / focused / anchor-only), recursive spatial modifiers (near/below/above/leftOf/rightOf/inside), and a Pattern / CompiledPattern split that gives JS-RegExp-like ergonomics with zero per-call compile cost on the resolver hot path.

§Quickstart

use smix_screen::{A11yNode, Rect, Role};
use smix_selector::{
    AnchorBox, IndexModifiers, Modifiers, Pattern, Selector,
    describe_selector, match_text, match_text_compiled,
};

// Plain text base
let s1: Selector = Selector::Text {
    text: Pattern::text("Login"),
    modifiers: Modifiers::default(),
};

// Spatial modifier — "Submit" below "Email"
let s2: Selector = Selector::Text {
    text: Pattern::text("Submit"),
    modifiers: Modifiers {
        below: Some(Box::new(Selector::Text {
            text: Pattern::text("Email"),
            modifiers: Modifiers::default(),
        })),
        ..Default::default()
    },
};

// Regex (auto case-insensitive via /(?i)/ prefix — maestro parity)
let s3 = Selector::Text {
    text: Pattern::regex("^log"),  // matches "Login", "Log out", etc.
    modifiers: Modifiers::default(),
};

// Anchor-only — pick the n-th element below a reference
let s4 = Selector::Anchor {
    anchor: AnchorBox {
        below: Some(Box::new(Selector::Text {
            text: Pattern::text("Header"),
            modifiers: Modifiers::default(),
        })),
        ..Default::default()
    },
    index: IndexModifiers { nth: Some(2), ..Default::default() },
};

// AI-readable rendering for failure prompts and logs
let s = describe_selector(&s2);
assert!(s.contains("below="));

// Hot-path matching: compile once, match many times
let pattern = Pattern::text("Login");
let compiled = pattern.compile().expect("text compile is infallible");
let node = A11yNode { /* ... */
};
assert!(match_text_compiled(&node, &compiled));

§Pattern / CompiledPattern split

The wire form of a text or regex selector pattern is Pattern (serde-friendly: a plain string for text, {regex, flags} object for regex). Matching uses CompiledPattern (caches the compiled regex::Regex DFA). Callers transition via Pattern::compile().

This split mirrors how V8’s RegExp lazily compiles + reuses the DFA once per object. In Rust we make the compile step explicit so the resolver layer can amortize compile cost across hundreds of candidate nodes in a tree walk:

OperationTS V8 baselineRust hot pathSpeedup
match_text string field-1 hit36.8 ns4.89 ns (match_text_compiled)7.5×
match_text string 6-field scan miss41.4 ns3.53 ns12×
match_text regex hit (default /i)105.5 ns9.39 ns11×
match_text regex on 100-char label52.2 ns31.5 ns1.7×
Pattern::compile regex (one-time, amortized)n/a15.65 µs

For one-shot SDK calls there is also match_text(&node, &Pattern) which compiles on every call (15 µs); resolver-driven hot loops should always prefer match_text_compiled.

§SDK convenience vs resolver hot path

smix-selector exposes two matcher entry points, and the choice between them matters by ~10000× for regex patterns:

APIWhenCost (regex)Cost (plain text)
match_text(node, &Pattern)One-shot ad-hoc calls; ergonomic for tests, REPLs, and tooling that matches a handful of nodes~15 µs / call (regex::Regex::new every time)~5 ns / call
match_text_compiled(node, &CompiledPattern)Hot loops; the real path inside smix-selector-resolver (HashMap<*const Pattern, CompiledPattern> cache + DFS) and smix-driver~5–50 ns / call (compile cost paid once)~5 ns / call

If you write your own resolver, walker, or any code that calls a matcher in a loop, reach for Pattern::compile() once at the call site and feed the resulting CompiledPattern to match_text_compiled per node — the pattern documented in smix-selector-resolver ResolverContext. A regression guard (crates/smix-selector/tests/perf_contract.rs) blocks bare hot-loop smix_selector::match_text calls from landing in workspace src/.

§When to reach for this

Use casePick
Need a structured query type for a11y trees / accessibility selectorssmix-selector
Want maestro-equivalent text matching (6-field OR + case-insensitive)smix-selector
Want recursive spatial modifiers (e.g. “Submit below Email”)smix-selector
Need xpath / CSS selectorsuse a generic query crate; this is iOS-shaped
Just need string matching, no treeuse regex directly

§Scope

  • ✅ 6 base forms × recursive spatial modifiers × index modifiers
  • ✅ camelCase JSON wire (no discriminator field; presence of text / id / label / role / focused / anchor is the discriminator)
  • Pattern::compile()CompiledPattern (regex cache)
  • match_text 6-field OR scan matching maestro parity
  • describe_selector AI-readable rendering
  • ❌ No tree walking (use smix-selector-resolver)
  • ❌ No xpath / CSS

§License

Dual-licensed under either:

at your option. smix-selector — Selector enum + Modifiers + AnchorBox + match_text (stone). Ported from now-retired TS source: src/core/selector.ts + src/core/resolve-selector.ts:153-184.

§Scope

  • Pure types (Selector, Pattern, Modifiers, AnchorBox, IndexModifiers) with serde wire compatibility (camelCase JSON, matching SDK / CLI / MCP / runner-client wire shape).
  • Pure functions (match_text, describe_selector) — no I/O, no Tree traversal (resolver lives in smix-selector-resolver, c5+).

§Selector base forms (跟 TS 同源, v1.5 C2 lock-in)

6 mutually exclusive base forms:

  1. Text (string / regex)
  2. Id (string strict equal vs node.identifier)
  3. Label (string strict equal vs node.label)
  4. Role { role, name?: pattern } (role enum + optional pattern name)
  5. Focused (focused: true — runtime-resolved current focus target)
  6. Anchor (anchor-only, no own base; spatial keys do the filtering)

§Modifiers / AnchorBox (recursive)

Base 1-4 stack Modifiers (6 spatial + 3 index). Base 6 (Anchor) stacks only IndexModifiers (no nested spatial — anchor IS the spatial intent already). Base 5 (Focused) stacks nothing.

§Pattern wire form (string-or-regex union, serde-friendly)

TS string | RegExp doesn’t survive JSON serialization (RegExp → {}). Wire form is:

  • "plain" (untagged string) — string strict equal (case-insensitive, v1.5 c5i-d maestro parity)
  • { "regex": "pattern", "flags": "i" } (object tag) — regex form, auto-/i flag inject if absent

Runtime compile happens inside match_text (no cache at the stone layer; resolver / SDK callers may memoize externally if needed).

Structs§

AnchorBox
Anchor base form’s spatial keys. Identical shape to the spatial half of Modifiers but without nth/first/last (Anchor stacks IndexModifiers separately).
IndexModifiers
IndexModifiers — the subset BaseAnchor can stack. Kept separate from Modifiers so anchor callers can’t accidentally write the top-level spatial form (which would be two surfaces for the same intent).
Modifiers
Modifier set stackable on Text / Id / Label / Role base forms. Spatial keys (near/below/above/leftOf/rightOf/inside) carry recursive selectors that resolve to an anchor node; index keys (nth/first/last) pick from the surviving candidate list.
True
Newtype around true — used as the Selector::Focused discriminator. Serializes/deserializes as the JSON literal true.

Enums§

CompiledPattern
Compiled, hot-path-ready pattern. Caches the compiled regex::Regex (跟 TS RegExp 对象内部 compile-once cache 同精神 — TS RegExp lazily compiles on first .test() and re-uses the compiled DFA; we eager- compile via Pattern::compile and store the result for the same effect on Rust side).
Pattern
String-or-regex pattern. Wire-compatible: plain JSON string ↔ Pattern::Text; tagged object {regex, flags}Pattern::Regex.
Role
Accessibility role enum (29 variants).
Selector
Selector — 6 mutually exclusive base forms.

Functions§

describe_selector
Human / AI-readable rendering of a Selector for error prompts and logs. Stable enough that AI can pattern-match against past failures. Mirrors TS describeSelector in src/core/selector.ts:118-160 1:1.
match_text
Match a Pattern against the six text-bearing fields of a node (label, title, value, placeholderValue, identifier, text) — OR semantics, case-insensitive. SDK convenience wrapper that compiles the pattern on every call; for hot loops prefer Pattern::compile once + match_text_compiled per node.
match_text_compiled
Text matching against an A11yNode. 1:1 with TS src/core/resolve-selector.ts:153-184 (matchText).