Use InputString instead of String

Done so that we can correctly support builds without an allocator.

Add two new error types that wrap `InputString`.
This commit is contained in:
Tobin C. Harding 2023-11-20 11:52:19 +11:00
parent acacf45edf
commit 396e049a7a
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
2 changed files with 52 additions and 22 deletions

View File

@ -14,8 +14,8 @@ exclude = ["tests", "contrib"]
[features] [features]
default = ["std"] default = ["std"]
std = ["alloc"] std = ["alloc", "internals/std"]
alloc = [] alloc = ["internals/alloc"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@ -12,6 +12,7 @@ use core::{default, ops};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use internals::error::InputString;
use internals::write_err; use internals::write_err;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
@ -129,12 +130,12 @@ impl FromStr for Denomination {
use self::ParseDenominationError::*; use self::ParseDenominationError::*;
if CONFUSING_FORMS.contains(&s) { if CONFUSING_FORMS.contains(&s) {
return Err(PossiblyConfusing(s.to_string())); return Err(PossiblyConfusing(PossiblyConfusingDenominationError(s.into())));
}; };
let form = self::Denomination::forms(s); let form = self::Denomination::forms(s);
form.ok_or_else(|| Unknown(s.to_string())) form.ok_or_else(|| Unknown(UnknownDenominationError(s.into())))
} }
} }
@ -196,9 +197,9 @@ impl From<ParseDenominationError> for ParseAmountError {
#[non_exhaustive] #[non_exhaustive]
pub enum ParseDenominationError { pub enum ParseDenominationError {
/// The denomination was unknown. /// The denomination was unknown.
Unknown(String), Unknown(UnknownDenominationError),
/// The denomination has multiple possible interpretations. /// The denomination has multiple possible interpretations.
PossiblyConfusing(String), PossiblyConfusing(PossiblyConfusingDenominationError),
} }
impl fmt::Display for ParseDenominationError { impl fmt::Display for ParseDenominationError {
@ -206,16 +207,8 @@ impl fmt::Display for ParseDenominationError {
use ParseDenominationError::*; use ParseDenominationError::*;
match *self { match *self {
Unknown(ref d) => write!(f, "unknown denomination: {}", d), Unknown(ref e) => write_err!(f, "denomination parse error"; e),
PossiblyConfusing(ref d) => { PossiblyConfusing(ref e) => write_err!(f, "denomination parse error"; e),
let (letter, upper, lower) = match d.chars().next() {
Some('M') => ('M', "Mega", "milli"),
Some('P') => ('P', "Peta", "pico"),
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it?
_ => panic!("invalid error information"),
};
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", letter, d, upper, lower)
}
} }
} }
} }
@ -231,6 +224,38 @@ impl std::error::Error for ParseDenominationError {
} }
} }
/// Parsing error, unknown denomination.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnknownDenominationError(InputString);
impl fmt::Display for UnknownDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.unknown_variant("bitcoin denomination", f)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnknownDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
/// Parsing error, possibly confusing denomination.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PossiblyConfusingDenominationError(InputString);
impl fmt::Display for PossiblyConfusingDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: possibly confusing denomination - we intentionally do not support 'M' and 'P' so as to not confuse mega/milli and peta/pico", self.0.display_cannot_parse("bitcoin denomination"))
}
}
#[cfg(feature = "std")]
impl std::error::Error for PossiblyConfusingDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
fn is_too_precise(s: &str, precision: usize) -> bool { fn is_too_precise(s: &str, precision: usize) -> bool {
match s.find('.') { match s.find('.') {
Some(pos) => Some(pos) =>
@ -1994,21 +2019,23 @@ mod tests {
#[test] #[test]
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
fn from_str() { fn from_str() {
use ParseDenominationError::*;
use super::ParseAmountError as E; use super::ParseAmountError as E;
assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x'))); assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x')));
assert_eq!( assert_eq!(
Amount::from_str("xBTC"), Amount::from_str("xBTC"),
Err(ParseDenominationError::Unknown("xBTC".into()).into()) Err(Unknown(UnknownDenominationError("xBTC".into())).into()),
); );
assert_eq!( assert_eq!(
Amount::from_str("5 BTC BTC"), Amount::from_str("5 BTC BTC"),
Err(ParseDenominationError::Unknown("BTC BTC".into()).into()) Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()),
); );
assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B'))); assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B')));
assert_eq!( assert_eq!(
Amount::from_str("5 5 BTC"), Amount::from_str("5 5 BTC"),
Err(ParseDenominationError::Unknown("5 BTC".into()).into()) Err(Unknown(UnknownDenominationError("5 BTC".into())).into()),
); );
#[track_caller] #[track_caller]
@ -2023,7 +2050,7 @@ mod tests {
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected);
} }
case("5 BCH", Err(ParseDenominationError::Unknown("BCH".into()).into())); case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".into())).into()));
case("-1 BTC", Err(E::Negative)); case("-1 BTC", Err(E::Negative));
case("-0.0 BTC", Err(E::Negative)); case("-0.0 BTC", Err(E::Negative));
@ -2133,7 +2160,10 @@ mod tests {
#[test] #[test]
fn to_string_with_denomination_from_str_roundtrip() { fn to_string_with_denomination_from_str_roundtrip() {
use ParseDenominationError::*;
use super::Denomination as D; use super::Denomination as D;
let amt = Amount::from_sat(42); let amt = Amount::from_sat(42);
let denom = Amount::to_string_with_denomination; let denom = Amount::to_string_with_denomination;
assert_eq!(Amount::from_str(&denom(amt, D::Bitcoin)), Ok(amt)); assert_eq!(Amount::from_str(&denom(amt, D::Bitcoin)), Ok(amt));
@ -2147,11 +2177,11 @@ mod tests {
assert_eq!( assert_eq!(
Amount::from_str("42 satoshi BTC"), Amount::from_str("42 satoshi BTC"),
Err(ParseDenominationError::Unknown("satoshi BTC".to_owned()).into()), Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
); );
assert_eq!( assert_eq!(
SignedAmount::from_str("-42 satoshi BTC"), SignedAmount::from_str("-42 satoshi BTC"),
Err(ParseDenominationError::Unknown("satoshi BTC".to_owned()).into()), Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
); );
} }