Implement support for `alloc`-free parse errors
This implements basic facilities to conditionally carry string inputs in parse errors. This includes: * `InputString` type that may carry the input and format it * `parse_error_type!` macro creating a special type for parse errors * `impl_parse` implementing parsing for various types as well as its `serde`-supporting alternative
This commit is contained in:
parent
783e1e81dc
commit
2b6bcf085c
|
@ -64,6 +64,9 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-internals"
|
name = "bitcoin-internals"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin_hashes"
|
name = "bitcoin_hashes"
|
||||||
|
|
|
@ -63,6 +63,9 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-internals"
|
name = "bitcoin-internals"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin_hashes"
|
name = "bitcoin_hashes"
|
||||||
|
|
|
@ -16,7 +16,7 @@ exclude = ["tests", "contrib"]
|
||||||
default = [ "std", "secp-recovery" ]
|
default = [ "std", "secp-recovery" ]
|
||||||
rand-std = ["secp256k1/rand-std"]
|
rand-std = ["secp256k1/rand-std"]
|
||||||
rand = ["secp256k1/rand"]
|
rand = ["secp256k1/rand"]
|
||||||
serde = ["actual-serde", "hashes/serde", "secp256k1/serde"]
|
serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde"]
|
||||||
secp-lowmemory = ["secp256k1/lowmemory"]
|
secp-lowmemory = ["secp256k1/lowmemory"]
|
||||||
secp-recovery = ["secp256k1/recovery"]
|
secp-recovery = ["secp256k1/recovery"]
|
||||||
bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]
|
bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]
|
||||||
|
|
|
@ -22,5 +22,6 @@ all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = { version = "1.0.103", default-features = false, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -26,7 +26,8 @@ fn main() {
|
||||||
|
|
||||||
// print cfg for all interesting versions less than or equal to minor
|
// print cfg for all interesting versions less than or equal to minor
|
||||||
// 46 adds `track_caller`
|
// 46 adds `track_caller`
|
||||||
for version in &[46] {
|
// 55 adds `kind()` to `ParseIntError`
|
||||||
|
for version in &[46, 55] {
|
||||||
if *version <= minor {
|
if *version <= minor {
|
||||||
println!("cargo:rustc-cfg=rust_v_1_{}", version);
|
println!("cargo:rustc-cfg=rust_v_1_{}", version);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
//! Error handling macros and helpers.
|
//! Error handling macros and helpers.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
|
pub mod input_string;
|
||||||
|
mod parse_error;
|
||||||
|
|
||||||
|
pub use input_string::InputString;
|
||||||
|
|
||||||
/// Formats error.
|
/// Formats error.
|
||||||
///
|
///
|
||||||
/// If `std` feature is OFF appends error source (delimited by `: `). We do this because
|
/// If `std` feature is OFF appends error source (delimited by `: `). We do this because
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
//! Implements the [`InputString`] type storing the parsed input.
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use storage::Storage;
|
||||||
|
|
||||||
|
/// Conditionally stores the input string in parse errors.
|
||||||
|
///
|
||||||
|
/// This type stores the input string of a parse function depending on whether `alloc` feature is
|
||||||
|
/// enabled. When it is enabled, the string is stored inside as `String`. When disabled this is a
|
||||||
|
/// zero-sized type and attempt to store a string does nothing.
|
||||||
|
///
|
||||||
|
/// This provides two methods to format the error strings depending on the context.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||||
|
pub struct InputString(Storage);
|
||||||
|
|
||||||
|
impl InputString {
|
||||||
|
/// Displays a message saying `failed to parse <self> as <what>`.
|
||||||
|
///
|
||||||
|
/// This is normally used whith the `write_err!` macro.
|
||||||
|
pub fn display_cannot_parse<'a, T>(&'a self, what: &'a T) -> CannotParse<'a, T>
|
||||||
|
where
|
||||||
|
T: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
CannotParse { input: self, what }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a message saying `<self> is not a known <what>`.
|
||||||
|
///
|
||||||
|
/// This is normally used in leaf parse errors (with no source) when parsing an enum.
|
||||||
|
pub fn unknown_variant<T>(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result
|
||||||
|
where
|
||||||
|
T: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
storage::unknown_variant(&self.0, what, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_from {
|
||||||
|
($($type:ty),+ $(,)?) => {
|
||||||
|
$(
|
||||||
|
impl From<$type> for InputString {
|
||||||
|
fn from(input: $type) -> Self {
|
||||||
|
#[allow(clippy::useless_conversion)]
|
||||||
|
InputString(input.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from!(&str);
|
||||||
|
|
||||||
|
/// Displays message saying `failed to parse <input> as <what>`.
|
||||||
|
///
|
||||||
|
/// This is created by `display_cannot_parse` method and should be used as
|
||||||
|
/// `write_err!("{}", self.input.display_cannot_parse("what is parsed"); self.source)` in parse
|
||||||
|
/// error [`Display`](fmt::Display) imlementation if the error has source. If the error doesn't
|
||||||
|
/// have a source just use regular `write!` with same formatting arguments.
|
||||||
|
pub struct CannotParse<'a, T: fmt::Display + ?Sized> {
|
||||||
|
input: &'a InputString,
|
||||||
|
what: &'a T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: fmt::Display + ?Sized> fmt::Display for CannotParse<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
storage::cannot_parse(&self.input.0, &self.what, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
mod storage {
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||||
|
pub(super) struct Storage;
|
||||||
|
|
||||||
|
impl fmt::Debug for Storage {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("<unknown input string - compiled without the `alloc` feature>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Storage {
|
||||||
|
fn from(_value: &str) -> Self { Storage }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cannot_parse<W>(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
|
||||||
|
where
|
||||||
|
W: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
write!(f, "failed to parse {}", what)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn unknown_variant<W>(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
|
||||||
|
where
|
||||||
|
W: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
write!(f, "unknown {}", what)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
mod storage {
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use super::InputString;
|
||||||
|
|
||||||
|
pub(super) type Storage = alloc::string::String;
|
||||||
|
|
||||||
|
pub(super) fn cannot_parse<W>(input: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
|
||||||
|
where
|
||||||
|
W: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
write!(f, "failed to parse '{}' as {}", input, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn unknown_variant<W>(inp: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result
|
||||||
|
where
|
||||||
|
W: fmt::Display + ?Sized,
|
||||||
|
{
|
||||||
|
write!(f, "'{}' is not a known {}", inp, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from!(alloc::string::String, alloc::boxed::Box<str>, alloc::borrow::Cow<'_, str>);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
//! Contains helpers for parsing-related errors.
|
||||||
|
|
||||||
|
/// Creates an error type intended for string parsing errors.
|
||||||
|
///
|
||||||
|
/// The resulting error type has two fields: `input` and `source`. The type of `input` is
|
||||||
|
/// [`InputString`](super::InputString), the type of `source` is specified as the second argument
|
||||||
|
/// to the macro.
|
||||||
|
///
|
||||||
|
/// The resulting type is public, conditionally implements [`std::error::Error`] and has a private
|
||||||
|
/// `new()` method for convenience.
|
||||||
|
///
|
||||||
|
/// ## Parameters
|
||||||
|
///
|
||||||
|
/// * `name` - the name of the error type
|
||||||
|
/// * `source` - the type of the source type
|
||||||
|
/// * `subject` - English description of the type being parsed (e.g. "a bitcoin amount")
|
||||||
|
/// * `derive` - list of derives to add
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! parse_error_type {
|
||||||
|
($vis:vis $name:ident, $source:ty, $subject:expr $(, $derive:path)* $(,)?) => {
|
||||||
|
#[derive(Debug $(, $derive)*)]
|
||||||
|
$vis struct $name {
|
||||||
|
input: $crate::error::InputString,
|
||||||
|
source: $source,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
/// Creates `Self`.
|
||||||
|
fn new<T: Into<$crate::error::InputString>>(input: T, source: $source) -> Self {
|
||||||
|
$name {
|
||||||
|
input: input.into(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for $name {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
$crate::error::write_err!("{}", self.input.display_cannot_parse($subject); self.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$crate::error::impl_std_error!($name, source);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ extern crate std;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod hex;
|
pub mod hex;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
mod parse;
|
||||||
|
pub mod serde;
|
||||||
|
|
||||||
/// Mainly reexports based on features.
|
/// Mainly reexports based on features.
|
||||||
pub(crate) mod prelude {
|
pub(crate) mod prelude {
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
/// Support for parsing strings.
|
||||||
|
|
||||||
|
// Impls a single TryFrom conversion
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_try_from_stringly {
|
||||||
|
($from:ty, $to:ty, $error:ty, $func:expr $(, $attr:meta)?) => {
|
||||||
|
$(#[$attr])?
|
||||||
|
impl core::convert::TryFrom<$from> for $to {
|
||||||
|
type Error = $error;
|
||||||
|
|
||||||
|
fn try_from(s: $from) -> Result<Self, Self::Error> {
|
||||||
|
$func(AsRef::<str>::as_ref(s)).map_err(|source| <$error>::new(s, source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements conversions from various string types.
|
||||||
|
///
|
||||||
|
/// This macro implements `FromStr` as well as `TryFrom<{stringly}` where `{stringly}` is one of
|
||||||
|
/// these types:
|
||||||
|
///
|
||||||
|
/// * `&str`
|
||||||
|
/// * `String`
|
||||||
|
/// * `Box<str>`
|
||||||
|
/// * `Cow<'_, str>`
|
||||||
|
///
|
||||||
|
/// The last three are only available with `alloc` feature turned on.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_parse {
|
||||||
|
($type:ty, $descr:expr, $func:expr, $vis:vis $error:ident, $error_source:ty $(, $error_derive:path)*) => {
|
||||||
|
$crate::parse_error_type!($vis $error, $error_source, $descr $(, $error_derive)*);
|
||||||
|
|
||||||
|
impl core::str::FromStr for $type {
|
||||||
|
type Err = $error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
$func(s).map_err(|source| <$error>::new(s, source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_try_from_stringly!(&str);
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
impl_try_from_stringly!(alloc::string::String, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc")));
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
impl_try_from_stringly!(alloc::borrow::Cow<'_, str>, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc")));
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
impl_try_from_stringly!(alloc::boxed::Box<str>, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements conversions from various string types as well as `serde` (de)serialization.
|
||||||
|
///
|
||||||
|
/// This calls `impl_parse` macro and implements serde deserialization by expecting and parsing a
|
||||||
|
/// string and serialization by outputting a string.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_parse_and_serde {
|
||||||
|
($type:ty, $descr:expr, $func:expr, $error:ident, $error_source:ty $(, $error_derive:path)*) => {
|
||||||
|
impl_parse!($type, $descr, $func, $error, $error_source $(, $error_derive)*);
|
||||||
|
|
||||||
|
// We don't use `serde_string_impl` because we want to avoid allocating input.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
||||||
|
impl<'de> $crate::serde::Deserialize<'de> for $type {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<$name, D::Error>
|
||||||
|
where
|
||||||
|
D: $crate::serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use core::fmt::{self, Formatter};
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
struct Visitor;
|
||||||
|
impl<'de> $crate::serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = $name;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str($descr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: $crate::serde::de::Error,
|
||||||
|
{
|
||||||
|
s.parse().map_err(|error| {
|
||||||
|
$crate::serde::IntoDeError::try_into_de_error(error)
|
||||||
|
.unwrap_or_else(|_| E::invalid_value(Unexpected::Str(s), &self))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
||||||
|
impl $crate::serde::Serialize for $name {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: $crate::serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.collect_str(&self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
//! Contains extensions of `serde` and internal reexports.
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Converts given error type to a type implementing [`de::Error`].
|
||||||
|
///
|
||||||
|
/// This is used in [`Deserialize`] implementations to convert specialized errors into serde
|
||||||
|
/// errors.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
pub trait IntoDeError: Sized {
|
||||||
|
/// Converts to deserializer error possibly outputting vague message.
|
||||||
|
///
|
||||||
|
/// This method is allowed to return a vague error message if the error type doesn't contain
|
||||||
|
/// enough information to explain the error precisely.
|
||||||
|
fn into_de_error<E: de::Error>(self, expected: Option<&dyn de::Expected>) -> E;
|
||||||
|
|
||||||
|
/// Converts to deserializer error without outputting vague message.
|
||||||
|
///
|
||||||
|
/// If the error type doesn't contain enough information to explain the error precisely this
|
||||||
|
/// should return `Err(self)` allowing the caller to use its information instead.
|
||||||
|
fn try_into_de_error<E>(self, expected: Option<&dyn de::Expected>) -> Result<E, Self>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(self.into_de_error(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
mod impls {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl IntoDeError for core::convert::Infallible {
|
||||||
|
fn into_de_error<E: de::Error>(self, _expected: Option<&dyn de::Expected>) -> E {
|
||||||
|
match self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDeError for core::num::ParseIntError {
|
||||||
|
fn into_de_error<E: de::Error>(self, expected: Option<&dyn de::Expected>) -> E {
|
||||||
|
self.try_into_de_error(expected).unwrap_or_else(|_| {
|
||||||
|
let expected = expected.unwrap_or(&"an integer");
|
||||||
|
|
||||||
|
E::custom(format_args!("invalid string, expected {}", expected))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(rust_v_1_55)]
|
||||||
|
fn try_into_de_error<E>(self, expected: Option<&dyn de::Expected>) -> Result<E, Self>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
use core::num::IntErrorKind::Empty;
|
||||||
|
|
||||||
|
let expected = expected.unwrap_or(&"an integer");
|
||||||
|
|
||||||
|
match self.kind() {
|
||||||
|
Empty => Ok(E::invalid_value(de::Unexpected::Str(""), expected)),
|
||||||
|
_ => Err(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(rust_v_1_55))]
|
||||||
|
fn try_into_de_error<E>(self, _expected: Option<&dyn de::Expected>) -> Result<E, Self>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Err(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue