Add ParseDenominationError

We have two variants in the `ParseAmountError` that both come from
parsing a denomination, these should be a separate error.
This commit is contained in:
Tobin C. Harding 2023-11-18 08:01:39 +11:00
parent 69e56a64ed
commit acacf45edf
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
4 changed files with 69 additions and 28 deletions

View File

@ -77,6 +77,7 @@ version = "0.1.0"
name = "bitcoin-units" name = "bitcoin-units"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitcoin-internals",
"serde", "serde",
"serde_json", "serde_json",
"serde_test", "serde_test",

View File

@ -76,6 +76,7 @@ version = "0.1.0"
name = "bitcoin-units" name = "bitcoin-units"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitcoin-internals",
"serde", "serde",
"serde_json", "serde_json",
"serde_test", "serde_test",

View File

@ -22,6 +22,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
internals = { package = "bitcoin-internals", version = "0.2.0" }
serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = 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::write_err;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use crate::prelude::{String, ToString}; use crate::prelude::{String, ToString};
@ -115,7 +116,7 @@ impl fmt::Display for Denomination {
} }
impl FromStr for Denomination { impl FromStr for Denomination {
type Err = ParseAmountError; type Err = ParseDenominationError;
/// Convert from a str to Denomination. /// Convert from a str to Denomination.
/// ///
@ -125,15 +126,15 @@ impl FromStr for Denomination {
/// ///
/// Due to ambiguity between mega and milli, pico and peta we prohibit usage of leading capital 'M', 'P'. /// Due to ambiguity between mega and milli, pico and peta we prohibit usage of leading capital 'M', 'P'.
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::ParseAmountError::*; use self::ParseDenominationError::*;
if CONFUSING_FORMS.contains(&s) { if CONFUSING_FORMS.contains(&s) {
return Err(PossiblyConfusingDenomination(s.to_string())); return Err(PossiblyConfusing(s.to_string()));
}; };
let form = self::Denomination::forms(s); let form = self::Denomination::forms(s);
form.ok_or_else(|| UnknownDenomination(s.to_string())) form.ok_or_else(|| Unknown(s.to_string()))
} }
} }
@ -153,10 +154,8 @@ pub enum ParseAmountError {
InputTooLarge, InputTooLarge,
/// Invalid character in input. /// Invalid character in input.
InvalidCharacter(char), InvalidCharacter(char),
/// The denomination was unknown. /// Invalid denomination.
UnknownDenomination(String), InvalidDenomination(ParseDenominationError),
/// The denomination has multiple possible interpretations.
PossiblyConfusingDenomination(String),
} }
impl fmt::Display for ParseAmountError { impl fmt::Display for ParseAmountError {
@ -170,8 +169,45 @@ impl fmt::Display for ParseAmountError {
InvalidFormat => f.write_str("invalid number format"), InvalidFormat => f.write_str("invalid number format"),
InputTooLarge => f.write_str("input string was too large"), InputTooLarge => f.write_str("input string was too large"),
InvalidCharacter(c) => write!(f, "invalid character in input: {}", c), InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d), InvalidDenomination(ref e) => write_err!(f, "invalid denomination"; e),
PossiblyConfusingDenomination(ref d) => { }
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseAmountError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseAmountError::*;
match *self {
Negative | TooBig | TooPrecise | InvalidFormat | InputTooLarge
| InvalidCharacter(_) => None,
InvalidDenomination(ref e) => Some(e),
}
}
}
impl From<ParseDenominationError> for ParseAmountError {
fn from(e: ParseDenominationError) -> Self { Self::InvalidDenomination(e) }
}
/// An error during amount parsing.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseDenominationError {
/// The denomination was unknown.
Unknown(String),
/// The denomination has multiple possible interpretations.
PossiblyConfusing(String),
}
impl fmt::Display for ParseDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ParseDenominationError::*;
match *self {
Unknown(ref d) => write!(f, "unknown denomination: {}", d),
PossiblyConfusing(ref d) => {
let (letter, upper, lower) = match d.chars().next() { let (letter, upper, lower) = match d.chars().next() {
Some('M') => ('M', "Mega", "milli"), Some('M') => ('M', "Mega", "milli"),
Some('P') => ('P', "Peta", "pico"), Some('P') => ('P', "Peta", "pico"),
@ -185,19 +221,12 @@ impl fmt::Display for ParseAmountError {
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for ParseAmountError { impl std::error::Error for ParseDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseAmountError::*; use ParseDenominationError::*;
match *self { match *self {
Negative Unknown(_) | PossiblyConfusing(_) => None,
| TooBig
| TooPrecise
| InvalidFormat
| InputTooLarge
| InvalidCharacter(_)
| UnknownDenomination(_)
| PossiblyConfusingDenomination(_) => None,
} }
} }
} }
@ -1968,10 +1997,19 @@ mod tests {
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!(Amount::from_str("xBTC"), Err(E::UnknownDenomination("xBTC".into()))); assert_eq!(
assert_eq!(Amount::from_str("5 BTC BTC"), Err(E::UnknownDenomination("BTC BTC".into()))); Amount::from_str("xBTC"),
Err(ParseDenominationError::Unknown("xBTC".into()).into())
);
assert_eq!(
Amount::from_str("5 BTC BTC"),
Err(ParseDenominationError::Unknown("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!(Amount::from_str("5 5 BTC"), Err(E::UnknownDenomination("5 BTC".into()))); assert_eq!(
Amount::from_str("5 5 BTC"),
Err(ParseDenominationError::Unknown("5 BTC".into()).into())
);
#[track_caller] #[track_caller]
fn case(s: &str, expected: Result<Amount, ParseAmountError>) { fn case(s: &str, expected: Result<Amount, ParseAmountError>) {
@ -1985,7 +2023,7 @@ mod tests {
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected);
} }
case("5 BCH", Err(E::UnknownDenomination("BCH".to_string()))); case("5 BCH", Err(ParseDenominationError::Unknown("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));
@ -2109,11 +2147,11 @@ mod tests {
assert_eq!( assert_eq!(
Amount::from_str("42 satoshi BTC"), Amount::from_str("42 satoshi BTC"),
Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), Err(ParseDenominationError::Unknown("satoshi BTC".to_owned()).into()),
); );
assert_eq!( assert_eq!(
SignedAmount::from_str("-42 satoshi BTC"), SignedAmount::from_str("-42 satoshi BTC"),
Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), Err(ParseDenominationError::Unknown("satoshi BTC".to_owned()).into()),
); );
} }
@ -2333,7 +2371,7 @@ mod tests {
for denom in confusing.iter() { for denom in confusing.iter() {
match Denomination::from_str(denom) { match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom), Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseAmountError::PossiblyConfusingDenomination(_)) => {} Err(ParseDenominationError::PossiblyConfusing(_)) => {}
Err(e) => panic!("unexpected error: {}", e), Err(e) => panic!("unexpected error: {}", e),
} }
} }
@ -2346,7 +2384,7 @@ mod tests {
for denom in unknown.iter() { for denom in unknown.iter() {
match Denomination::from_str(denom) { match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom), Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseAmountError::UnknownDenomination(_)) => {} Err(ParseDenominationError::Unknown(_)) => {}
Err(e) => panic!("unexpected error: {}", e), Err(e) => panic!("unexpected error: {}", e),
} }
} }