Skip to content

refactor: simplify NodeValue API to 2 methods, fix AttrParser JID bug - #386

Merged
jlucaso1 merged 3 commits into
mainfrom
refactor/simplify-nodevalue-api
Mar 18, 2026
Merged

refactor: simplify NodeValue API to 2 methods, fix AttrParser JID bug#386
jlucaso1 merged 3 commits into
mainfrom
refactor/simplify-nodevalue-api

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Reduce NodeValue public API from 4 methods to 2 — eliminates variant-specific footguns
  • Fix latent bug in AttrParser::optional_string() where JID-typed attributes silently returned None
  • Migrate 20 comparison sites from verbose optional_string().as_deref() == Some("x") to zero-copy attrs.get("k").is_some_and(|v| v == "x")
  • Use as_str().into_owned() instead of to_string() for NodeValue→String extraction (avoids format! overhead)
  • Simplify PresenceHandler: use to_jid() directly instead of to_string() + parse() roundtrip

NodeValue API (final: 2 methods + 2 traits)

API Purpose Cost
as_str() -> Cow<str> String view (both variants) Zero-copy for String, formats Jid
to_jid() -> Option<Jid> JID extraction (both variants) Clone or parse
PartialEq<str> Comparison (both variants) Zero-copy always
Display Formatting (both variants) Streaming, no alloc

Removed: as_jid(), to_string_value(), deprecated string()

Bug fixed

AttrParser::optional_string("from") returned None when the attribute was JID-typed (which happens after binary decoding). Now returns Some(Cow::Owned(formatted_jid)).

Comparison migration

// Before (verbose + AttrParser overhead):
node.attrs().optional_string("type").as_deref() == Some("w:gp2")

// After (zero-copy + no overhead):
node.attrs.get("type").is_some_and(|v| v == "w:gp2")

20 comparison sites migrated. Remaining .as_deref() sites are value extractions (not comparisons) or AttrParser API tests.

Test plan

  • cargo test --all --exclude e2e-tests — 874 tests pass
  • cargo clippy --all --tests — zero warnings
  • cargo fmt --all — clean
  • Verify no behavioral change in connected session

Summary by CodeRabbit

  • Refactor
    • Improved internal attribute extraction and string handling patterns across the codebase to enhance memory safety and code maintainability.
    • Strengthened option-handling semantics to reduce potential runtime issues and align with idiomatic Rust practices.

…dling

NodeValue public API reduced from 4 methods to 2:
- as_str() -> Cow<'_, str> (was Option<&str>) — works for both String
  and Jid variants; zero-copy for String, formats Jid on demand
- to_jid() -> Option<Jid> — unchanged, already works for both variants

Removed methods:
- as_jid() — replaced by to_jid() which handles both variants
- to_string_value() — replaced by as_str().into_owned() or Display

AttrParser fixes:
- optional_string() now returns Option<Cow<str>> instead of Option<&str>,
  fixing a latent bug where JID-typed attributes silently returned None
- required_string() follows the same Cow<str> return type
- Removed deprecated string() method

All callers migrated to use PartialEq<str> for comparisons (zero-copy)
and Cow<str> for string extraction.
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Refactors how string attributes and node values are represented and accessed: optional/required string APIs now use Cow<> and NodeValue::as_str returns Cow; call sites updated to use as_deref(), into_owned(), is_some_and(), and related patterns across handlers, wacore, and tests.

Changes

Cohort / File(s) Summary
Core binary API
wacore/binary/src/attrs.rs, wacore/binary/src/node.rs
optional/required string APIs switched to Cow<'a, str>; removed string()/as_jid()/to_string_value(); NodeValue::as_str() now returns Cow.
IQ & wacore parsing
wacore/src/iq/..., wacore/src/request.rs, wacore/src/iq/node.rs
Updated attribute access to .as_deref(), .into_owned() and .is_some_and(); optional_attr/required_attr signatures adapted to Cow. Tests adjusted accordingly.
Stanza & business logic
wacore/src/stanza/..., wacore/src/pair.rs, wacore/src/reporting_token.rs
Rewrote optional attribute handling to use as_deref()/into_owned() and predicate checks; minor node-building string conversion changes.
Client / Handlers / Message processing
src/client.rs, src/handlers/..., src/message.rs, src/request.rs, src/receipt.rs, src/pair.rs
Replaced chained .and_then(... .as_str()) patterns with .as_deref() or .is_some_and() checks; adjusted ID/type/xmlns/name extraction and defaults.
Presence, retry, unified session
src/features/presence.rs, src/retry.rs, src/unified_session.rs, wacore/appstate/src/patch_decode.rs
Applied as_deref() for optional strings and is_some_and() for type checks; refined error/code parsing paths.
Spam/reporting/encryption tests
src/spam_report.rs, wacore/src/types/spam_report.rs, src/types/enc_handler.rs, wacore/tests/binary_protocol_test.rs
Test assertions migrated to get(...).is_some_and(...) or .as_deref() patterns; enc type/default handling adjusted to as_deref().unwrap_or(...).
End-to-end & other tests
tests/e2e/tests/*, many wacore/src/... tests
Updated many tests to use borrowed comparisons (.as_deref()) and predicate-based checks (.is_some_and(...)) matching the new Cow-based APIs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 In carrot code I hop and play,

Cow strings nibble bugs away,
No more moves that made me sigh,
as_deref and is_some fly!
Patch complete — I thump, hooray! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: simplifying the NodeValue API to 2 methods and fixing an AttrParser JID bug, which aligns with the substantial API refactoring across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/simplify-nodevalue-api
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 18, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchrefactor/simplify-nodevalue-api
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,197.00
(-7.92%)Baseline: 6,730.17
7,066.68
(87.69%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-38.18%)Baseline: 848,080.93
890,484.97
(58.88%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-9.11%)Baseline: 22,959.41
24,107.38
(86.56%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,193.00
(-23.12%)Baseline: 127,725.35
134,111.62
(73.22%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,221.00
(-17.48%)Baseline: 119,033.83
124,985.52
(78.59%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.20%)Baseline: 534,031.78
560,733.37
(95.04%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,862.00
(-8.56%)Baseline: 17,346.77
18,214.10
(87.09%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,713,533.00
(-14.05%)Baseline: 17,118,806.09
17,974,746.39
(81.86%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,349.00
(-32.79%)Baseline: 176,087.98
184,892.38
(64.01%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.20%)Baseline: 535,444.60
562,216.83
(95.05%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,911.00
(-7.68%)Baseline: 19,400.10
20,370.10
(87.93%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,063,047.00
(-34.16%)Baseline: 42,625,810.70
44,757,101.23
(62.70%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.20%)Baseline: 534,470.78
561,194.32
(95.04%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,835.00
(-11.63%)Baseline: 17,918.68
18,814.62
(84.16%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,714,965.00
(-14.05%)Baseline: 17,119,658.39
17,975,641.31
(81.86%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
108,094.00
(-18.54%)Baseline: 132,699.05
139,334.00
(77.58%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,293.00
(-17.47%)Baseline: 119,105.83
125,061.12
(78.60%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-8.00%)Baseline: 98,882.41
103,826.53
(87.62%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-5.20%)Baseline: 7,782.75
8,171.89
(90.29%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-3.32%)Baseline: 94,128.13
98,834.54
(92.08%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.68%)Baseline: 7,350.83
7,718.38
(95.89%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-2.84%)Baseline: 109,913.13
115,408.79
(92.53%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.57%)Baseline: 8,862.83
9,305.98
(95.78%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-11.25%)Baseline: 47,310.31
49,675.82
(84.53%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-6.39%)Baseline: 2,902.33
3,047.44
(89.16%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+3.70%)Baseline: 536,251.12
563,063.68
(98.76%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.45%)Baseline: 774.45
813.18
(94.81%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,570,336.00
(-0.53%)Baseline: 27,717,883.82
29,103,778.01
(94.73%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,540,322.00
(-0.17%)Baseline: 5,549,972.13
5,827,470.74
(95.07%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
178,094.00
(+0.03%)Baseline: 178,042.67
186,944.81
(95.27%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
178,905.00
(+0.03%)Baseline: 178,854.01
187,796.71
(95.27%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,189,407.00
(-0.55%)Baseline: 17,283,981.63
18,148,180.71
(94.72%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
295,884.00
(+0.01%)Baseline: 295,846.16
310,638.47
(95.25%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,545,174.00
(-0.42%)Baseline: 12,598,280.22
13,228,194.23
(94.84%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
715,555.00
(-0.02%)Baseline: 715,707.53
751,492.91
(95.22%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
41,823.00
(+0.03%)Baseline: 41,811.63
43,902.22
(95.26%)
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,711.51
16,339,797.08
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,504,859.00
(-0.11%)Baseline: 5,511,100.17
5,786,655.18
(95.13%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
956,774.00
(-0.22%)Baseline: 958,913.60
1,006,859.28
(95.03%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,822,723.00
(-0.01%)Baseline: 2,822,878.14
2,964,022.04
(95.23%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,444,364.00
(-1.12%)Baseline: 3,483,367.03
3,657,535.38
(94.17%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
124,954,925.00
(-0.36%)Baseline: 125,403,610.67
131,673,791.20
(94.90%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,812.00
(+0.12%)Baseline: 11,797.77
12,387.66
(95.35%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,825.00
(+0.04%)Baseline: 3,823.32
4,014.49
(95.28%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,720.00
(-0.27%)Baseline: 87,954.54
92,352.26
(94.98%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,754.00
(-0.31%)Baseline: 80,002.96
84,003.10
(94.94%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,011.00
(-0.02%)Baseline: 51,020.97
53,572.02
(95.22%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,762.00
(+0.27%)Baseline: 5,746.64
6,033.97
(95.49%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,122.00
(+0.19%)Baseline: 2,118.01
2,223.91
(95.42%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.02%)Baseline: 21,915.63
23,011.41
(95.26%)
🐰 View full continuous benchmarking report in Bencher

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/iq/groups.rs`:
- Around line 344-348: The current participant_type assignment in groups.rs
collapses missing and invalid "type" values into ParticipantType::Member; change
it to first check node.attrs().optional_string("type") and only default to
Member when that returns None, but when it returns Some(s) attempt
ParticipantType::try_from(s.as_ref()) and surface/return the parse error if
try_from fails (do not unwrap_or), updating the surrounding function's error
handling to propagate that parse error; look for the participant_type variable
assignment and calls to node.attrs().optional_string("type") and
ParticipantType::try_from to implement this behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39438e7e-fff8-4f97-8a36-f038339743ea

📥 Commits

Reviewing files that changed from the base of the PR and between 2af8e3d and 9363e32.

📒 Files selected for processing (43)
  • src/client.rs
  • src/features/presence.rs
  • src/handlers/iq.rs
  • src/handlers/notification.rs
  • src/message.rs
  • src/pair.rs
  • src/receipt.rs
  • src/request.rs
  • src/retry.rs
  • src/spam_report.rs
  • src/types/enc_handler.rs
  • src/unified_session.rs
  • tests/e2e/tests/groups.rs
  • tests/e2e/tests/memory_soak.rs
  • tests/e2e/tests/offline_groups.rs
  • tests/e2e/tests/receipts.rs
  • wacore/appstate/src/patch_decode.rs
  • wacore/binary/src/attrs.rs
  • wacore/binary/src/node.rs
  • wacore/derive/src/lib.rs
  • wacore/src/ib.rs
  • wacore/src/iq/blocklist.rs
  • wacore/src/iq/chatstate.rs
  • wacore/src/iq/contacts.rs
  • wacore/src/iq/dirty.rs
  • wacore/src/iq/groups.rs
  • wacore/src/iq/mediaconn.rs
  • wacore/src/iq/mex.rs
  • wacore/src/iq/node.rs
  • wacore/src/iq/prekeys.rs
  • wacore/src/iq/privacy.rs
  • wacore/src/iq/props.rs
  • wacore/src/iq/spam_report.rs
  • wacore/src/iq/tctoken.rs
  • wacore/src/iq/usync.rs
  • wacore/src/pair.rs
  • wacore/src/reporting_token.rs
  • wacore/src/request.rs
  • wacore/src/stanza/business.rs
  • wacore/src/stanza/devices.rs
  • wacore/src/stanza/groups.rs
  • wacore/src/types/spam_report.rs
  • wacore/tests/binary_protocol_test.rs

Comment thread wacore/src/iq/groups.rs
Comment on lines +344 to 348
let participant_type = node
.attrs()
.optional_string("type")
.and_then(|s| ParticipantType::try_from(s.as_ref()).ok())
.unwrap_or(ParticipantType::Member);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle invalid participant types separately from missing values.

This currently maps both missing and invalid type values to Member, which can silently misclassify server responses and hide protocol changes. Keep the default only for missing values, and surface invalid values as parse errors.

🔧 Proposed fix
-        let participant_type = node
-            .attrs()
-            .optional_string("type")
-            .and_then(|s| ParticipantType::try_from(s.as_ref()).ok())
-            .unwrap_or(ParticipantType::Member);
+        let participant_type = match node.attrs().optional_string("type").as_deref() {
+            None => ParticipantType::Member,
+            Some(raw) => ParticipantType::try_from(Some(raw))?,
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let participant_type = node
.attrs()
.optional_string("type")
.and_then(|s| ParticipantType::try_from(s.as_ref()).ok())
.unwrap_or(ParticipantType::Member);
let participant_type = match node.attrs().optional_string("type").as_deref() {
None => ParticipantType::Member,
Some(raw) => ParticipantType::try_from(raw)?,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/groups.rs` around lines 344 - 348, The current participant_type
assignment in groups.rs collapses missing and invalid "type" values into
ParticipantType::Member; change it to first check
node.attrs().optional_string("type") and only default to Member when that
returns None, but when it returns Some(s) attempt
ParticipantType::try_from(s.as_ref()) and surface/return the parse error if
try_from fails (do not unwrap_or), updating the surrounding function's error
handling to propagate that parse error; look for the participant_type variable
assignment and calls to node.attrs().optional_string("type") and
ParticipantType::try_from to implement this behavior.

…trs.get() + PartialEq

Replace 19 instances of the verbose pattern:
  node.attrs().optional_string("key").as_deref() == Some("value")
with the cleaner zero-copy pattern:
  node.attrs.get("key").is_some_and(|v| v == "value")

This bypasses AttrParser entirely for simple comparisons, using
NodeValue's PartialEq<str> directly — zero-copy for both String
and Jid variants, no error tracking overhead.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 18, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 2m 43s —— View job


Code Review: Simplify NodeValue API

  • Read core API changes (attrs.rs, node.rs)
  • Review derive macro changes
  • Review caller migration patterns
  • Check for correctness issues
  • Post final review

Overall: This is a clean, well-executed refactoring. The API surface reduction is sound, the JID bug fix is correct, and the comparison migrations are consistently applied. CI is green with benchmarks showing no regressions (many benchmarks actually improved). A few observations below.

Core API changes — clean

The NodeValue API simplification from 4 methods to 2 (as_str() -> Cow, to_jid()) + 2 traits (PartialEq<str>, Display) is well-designed. The zero-copy EqCheck writer in PartialEq<str> for JID comparison (node.rs:78-108) is clever and avoids allocation.

The AttrParser::optional_string() bug fix (returning None for JID-typed attrs) is the most important behavioral change — now correctly maps through v.as_str() / v.to_string_cow() which handles both variants.

Derive macro changes — correct

wacore/derive/src/lib.rs — The parse_string_enum calls are correctly updated:

  • node.attrs().optional_string(#attr_name).as_deref().unwrap_or(#default).as_deref() converts Option<Cow<str>> to Option<&str>, correct.
  • &node.attrs().optional_string(#attr_name).ok_or_else(...)? &Cow<str> auto-derefs to &str for the parse_string_enum(&str) call, correct.

Minor observations

1. to_string() vs to_string_value() in src/request.rs:211

let id_opt = node.attrs.get("id").map(|v| v.to_string());

This now goes through Display which is functionally equivalent to the removed to_string_value(). However, to_string() allocates via format!() machinery even for the String variant, whereas the old to_string_value() did a direct .clone(). For an IQ response ID (always string-typed, called on every IQ), this is a minor perf regression. Consider using v.as_str().into_owned() instead. Not blocking.

2. CodeRabbit's ParticipantType concern — intentionally lenient, not a bug (wacore/src/iq/groups.rs:344-348)

let participant_type = node
    .attrs()
    .optional_string("type")
    .and_then(|s| ParticipantType::try_from(s.as_ref()).ok())
    .unwrap_or(ParticipantType::Member);

CodeRabbit flagged that invalid types silently become Member. However, the old code (TryFrom<Option<&str>>) already mapped None and "member" to Member, and the comment says "avoid failing the whole group parse." This is a deliberate design choice for protocol forward-compatibility — if WhatsApp adds a new participant type, silently defaulting is better than crashing the entire group info parse. I'd keep it as-is.

That said, note the TryFrom<Option<&str>> impl at line 80 handles None => Member already, so the old code ParticipantType::try_from(node.attrs().optional_string("type")) was a single call. The new code is equivalent but slightly more verbose. Both are fine.

3. Consistent .as_deref().unwrap_or_default().to_string() pattern

Several sites follow this pattern for extracting owned strings from optional attrs:

node.attrs().optional_string("subject").as_deref().unwrap_or_default().to_string()

This is correct but creates a temporary Cow then derefs then allocates. An alternative would be optional_string("subject").map(|s| s.into_owned()).unwrap_or_default() which avoids the deref hop. Not blocking — both are clear enough.

4. src/message.rs:487 — mixed patterns in same function

Lines 437-441 use the new attrs.get("decrypt-fail").is_some_and(|v| v == "hide") pattern, but lines 484-486 still use optional_string("type").as_deref() == Some("skmsg"). Both work, but the inconsistency within the same function is slightly jarring. Minor nit.

Summary

Solid refactoring that reduces API surface, fixes a real bug (JID attrs silently returning None), and improves ergonomics across 43 files. The zero-copy PartialEq<str> implementation is a nice touch. All benchmarks pass within bounds.

…g comparisons

- Use as_str().into_owned() instead of to_string() for NodeValue→String
  extraction (avoids format! machinery for String variant)
- Migrate skmsg type check to attrs.get() + PartialEq pattern
- Simplify PresenceHandler: use to_jid() directly instead of
  to_string() + parse(), use is_some_and for type comparison
@jlucaso1
jlucaso1 merged commit 7eee3d2 into main Mar 18, 2026
7 of 8 checks passed
@jlucaso1
jlucaso1 deleted the refactor/simplify-nodevalue-api branch March 18, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant