refactor: Use regular functions rather than FromStr impls (#8178)

Implementing `FromStr` and then calling `parse()` creates an
indirection, which is hard to follow for people who are not familiar
with Rust. @r10s recently voiced this problem when we were
pair-programming, and I agree.

We can decide what exactly to call the new function.

I didn't remove all `FromStr` implementations yet; `FromStr for
Fingerprint`, `FromStr for Params`, `FromStr for MozConfigTag` and
`FromStr for EphemeralTimer` are still left.
This commit is contained in:
Hocuri
2026-04-27 11:09:01 +02:00
committed by GitHub
parent 63f96d9138
commit 0580056b62
4 changed files with 34 additions and 42 deletions

View File

@@ -4,9 +4,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::str::FromStr;
use anyhow::{Context as _, Error, Result, bail}; use anyhow::{Context as _, Result, bail};
use crate::key::{DcKey, SignedPublicKey}; use crate::key::{DcKey, SignedPublicKey};
@@ -28,10 +27,8 @@ impl fmt::Display for EncryptPreference {
} }
} }
impl FromStr for EncryptPreference { impl EncryptPreference {
type Err = Error; fn new(s: &str) -> Result<Self> {
fn from_str(s: &str) -> Result<Self> {
match s { match s {
"mutual" => Ok(EncryptPreference::Mutual), "mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference), "nopreference" => Ok(EncryptPreference::NoPreference),
@@ -85,10 +82,8 @@ impl fmt::Display for Aheader {
} }
} }
impl FromStr for Aheader { impl Aheader {
type Err = Error; pub(crate) fn from_str(s: &str) -> Result<Self> {
fn from_str(s: &str) -> Result<Self> {
let mut attributes: BTreeMap<String, String> = s let mut attributes: BTreeMap<String, String> = s
.split(';') .split(';')
.filter_map(|a| { .filter_map(|a| {
@@ -116,7 +111,7 @@ impl FromStr for Aheader {
let prefer_encrypt = attributes let prefer_encrypt = attributes
.remove("prefer-encrypt") .remove("prefer-encrypt")
.and_then(|raw| raw.parse().ok()) .and_then(|raw| EncryptPreference::new(&raw).ok())
.unwrap_or_default(); .unwrap_or_default();
let verified = attributes.remove("_verified").is_some(); let verified = attributes.remove("_verified").is_some();
@@ -144,8 +139,9 @@ mod tests {
#[test] #[test]
fn test_from_str() -> Result<()> { fn test_from_str() -> Result<()> {
let h: Aheader = let h = Aheader::from_str(&format!(
format!("addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}").parse()?; "addr=me@mail.com; prefer-encrypt=mutual; keydata={RAWKEY}"
))?;
assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual); assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
@@ -157,7 +153,7 @@ mod tests {
#[test] #[test]
fn test_from_str_reset() -> Result<()> { fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}"); let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
let h: Aheader = raw.parse()?; let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "reset@example.com"); assert_eq!(h.addr, "reset@example.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference); assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -167,7 +163,7 @@ mod tests {
#[test] #[test]
fn test_from_str_non_critical() -> Result<()> { fn test_from_str_non_critical() -> Result<()> {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}"); let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={RAWKEY}");
let h: Aheader = raw.parse()?; let h = Aheader::from_str(&raw)?;
assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference); assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
@@ -177,7 +173,7 @@ mod tests {
#[test] #[test]
fn test_from_str_superflous_critical() { fn test_from_str_superflous_critical() {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}"); let raw = format!("addr=me@mail.com; _foo=one; _bar=two; other=me; keydata={RAWKEY}");
assert!(raw.parse::<Aheader>().is_err()); assert!(Aheader::from_str(&raw).is_err());
} }
#[test] #[test]

View File

@@ -2127,7 +2127,7 @@ async fn parse_gossip_headers(
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default(); let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
for value in &gossip_headers { for value in &gossip_headers {
let header = match value.parse::<Aheader>() { let header = match Aheader::from_str(value) {
Ok(header) => header, Ok(header) => header,
Err(err) => { Err(err) => {
warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err); warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);

View File

@@ -36,10 +36,7 @@ pub struct Reaction {
reaction: String, reaction: String,
} }
// We implement From<&str> instead of std::str::FromStr, because impl Reaction {
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
/// Convert a `&str` into a `Reaction`. /// Convert a `&str` into a `Reaction`.
/// Everything after the first whitespace is ignored. /// Everything after the first whitespace is ignored.
/// ///
@@ -51,7 +48,7 @@ impl From<&str> for Reaction {
/// reactions is not different from other kinds of spam attacks /// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be /// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user. /// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self { pub fn new(reaction: &str) -> Self {
let reaction: &str = reaction let reaction: &str = reaction
.split_ascii_whitespace() .split_ascii_whitespace()
.next() .next()
@@ -61,9 +58,7 @@ impl From<&str> for Reaction {
reaction: reaction.to_string(), reaction: reaction.to_string(),
} }
} }
}
impl Reaction {
/// Returns true if reaction contains no emoji. /// Returns true if reaction contains no emoji.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.reaction.is_empty() self.reaction.is_empty()
@@ -212,7 +207,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) ->
let msg = Message::load_from_db(context, msg_id).await?; let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id; let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into(); let reaction = Reaction::new(reaction);
let mut reaction_msg = Message::new_text(reaction.as_str().to_string()); let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
reaction_msg.set_reaction(); reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid); reaction_msg.in_reply_to = Some(msg.rfc724_mid);
@@ -282,7 +277,7 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
|row| { |row| {
let contact_id: ContactId = row.get(0)?; let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?; let reaction: String = row.get(1)?;
Ok((contact_id, Reaction::from(reaction.as_str()))) Ok((contact_id, Reaction::new(reaction.as_str())))
}, },
) )
.await?; .await?;
@@ -361,32 +356,32 @@ mod tests {
#[test] #[test]
fn test_parse_reaction() { fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported. // Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").as_str(), "👍"); assert_eq!(Reaction::new("👍").as_str(), "👍");
assert_eq!(Reaction::from("👎").as_str(), "👎"); assert_eq!(Reaction::new("👎").as_str(), "👎");
assert_eq!(Reaction::from("😀").as_str(), "😀"); assert_eq!(Reaction::new("😀").as_str(), "😀");
assert_eq!(Reaction::from("").as_str(), ""); assert_eq!(Reaction::new("").as_str(), "");
assert_eq!(Reaction::from("😢").as_str(), "😢"); assert_eq!(Reaction::new("😢").as_str(), "😢");
// Empty string can be used to remove all reactions. // Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty()); assert!(Reaction::new("").is_empty());
// Short strings can be used as emojis, could be used to add // Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes. // support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").as_str(), ":deltacat:"); assert_eq!(Reaction::new(":deltacat:").as_str(), ":deltacat:");
// Check that long strings are not valid emojis. // Check that long strings are not valid emojis.
assert!( assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty() Reaction::new(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
); );
// Multiple reactions separated by spaces or tabs are not supported. // Multiple reactions separated by spaces or tabs are not supported.
assert_eq!(Reaction::from("👍 ❤").as_str(), "👍"); assert_eq!(Reaction::new("👍 ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍\t").as_str(), "👍"); assert_eq!(Reaction::new("👍\t").as_str(), "👍");
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍"); assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), "👍"); assert_eq!(Reaction::new("👍\t:foo: ❤").as_str(), "👍");
assert_eq!(Reaction::from("👍 👍").as_str(), "👍"); assert_eq!(Reaction::new("👍 👍").as_str(), "👍");
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -582,7 +577,7 @@ Content-Disposition: reaction\n\
assert_eq!(chat_id, expected_chat_id); assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id); assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id); assert_eq!(contact_id, expected_contact_id);
assert_eq!(reaction, Reaction::from(expected_reaction)); assert_eq!(reaction, Reaction::new(expected_reaction));
} }
_ => panic!("Unexpected event {event:?}."), _ => panic!("Unexpected event {event:?}."),
} }

View File

@@ -3,6 +3,7 @@
use std::cmp; use std::cmp;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::iter; use std::iter;
use std::str::FromStr as _;
use std::sync::LazyLock; use std::sync::LazyLock;
use anyhow::{Context as _, Result, ensure}; use anyhow::{Context as _, Result, ensure};
@@ -1828,7 +1829,7 @@ async fn add_parts(
// Extract ephemeral timer from the message // Extract ephemeral timer from the message
let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer) let mut ephemeral_timer = if let Some(value) = mime_parser.get_header(HeaderDef::EphemeralTimer)
{ {
match value.parse::<EphemeralTimer>() { match EphemeralTimer::from_str(value) {
Ok(timer) => timer, Ok(timer) => timer,
Err(err) => { Err(err) => {
warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}."); warn!(context, "Can't parse ephemeral timer \"{value}\": {err:#}.");
@@ -2106,7 +2107,7 @@ async fn add_parts(
chat_id, chat_id,
from_id, from_id,
sort_timestamp, sort_timestamp,
Reaction::from(reaction_str.as_str()), Reaction::new(reaction_str.as_str()),
is_incoming_fresh, is_incoming_fresh,
) )
.await?; .await?;