feat(api)!: add distinct error type (#1)

* feat(api)!: add distinct error type

The commit adds an `Error` enum to the public API of the opml crate to
make it easier to reason about what went wrong during OPML
parsing/writing.
This commit is contained in:
SirWindfield 2021-03-22 12:41:11 +01:00 committed by GitHub
parent f1a4f57ec2
commit 312627e298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 57 additions and 36 deletions

37
Cargo.lock generated
View File

@ -121,6 +121,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"strong-xml", "strong-xml",
"thiserror",
] ]
[[package]] [[package]]
@ -162,9 +163,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.19" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@ -235,9 +236,9 @@ dependencies = [
[[package]] [[package]]
name = "strong-xml" name = "strong-xml"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee06e7e5baf4508dea83506a83fcc5b80a404d4c0e9c473c9a4b38b802af3a07" checksum = "da0e355b33893aa1b6d2f29110e729609c87a4241817a3e53ffba4c31421b1b1"
dependencies = [ dependencies = [
"jetscii", "jetscii",
"lazy_static", "lazy_static",
@ -248,9 +249,9 @@ dependencies = [
[[package]] [[package]]
name = "strong-xml-derive" name = "strong-xml-derive"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e4e25fb64e61f55d495134d9e5ac68b1fa4bb2855b5a5b53857b9460e2bfde" checksum = "e9053670189294a3fa7d7379a58b07ab0d0608debe80f5a16d31e2faa6e13b69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -265,9 +266,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.39" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -283,6 +284,26 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "thiserror"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.0.1" version = "1.0.1"

View File

@ -17,4 +17,5 @@ path = "source/lib.rs"
[dependencies] [dependencies]
regex = "1.3" regex = "1.3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
strong-xml = "0.6" strong-xml = "0.6.1"
thiserror = "1.0.24"

View File

@ -61,10 +61,23 @@
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strong_xml::{XmlError, XmlRead, XmlWrite}; use strong_xml::{XmlRead, XmlWrite};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("OPML body has no <outlines> elements")]
BodyHasNoOutlines,
#[error("Unsupported OPML version: {0:?}")]
UnsupportedVersion(String),
#[error("Failed to process XML file")]
XmlError(#[from] strong_xml::XmlError),
}
/// The top-level [OPML](struct.OPML.html) element. /// The top-level [OPML](struct.OPML.html) element.
#[derive(XmlWrite, XmlRead, PartialEq, Debug, Clone, Serialize, Deserialize)] #[derive(
XmlWrite, XmlRead, PartialEq, Debug, Clone, Serialize, Deserialize,
)]
#[xml(tag = "opml")] #[xml(tag = "opml")]
pub struct OPML { pub struct OPML {
/// The version attribute from the element, valid values are `1.0`, `1.1` and `2.0`. /// The version attribute from the element, valid values are `1.0`, `1.1` and `2.0`.
@ -99,13 +112,8 @@ impl OPML {
/// ///
/// assert_eq!(parsed, expected); /// assert_eq!(parsed, expected);
/// ``` /// ```
pub fn new(xml: &str) -> Result<Self, String> { pub fn new(xml: &str) -> Result<Self, Error> {
let opml: Result<OPML, XmlError> = OPML::from_str(xml); let opml = OPML::from_str(xml)?;
let opml = match opml {
Ok(value) => value,
Err(err) => return Err(format!("XML parsing error: {:#?}", err)),
};
let version = &opml.version; let version = &opml.version;
@ -116,15 +124,12 @@ impl OPML {
if !valid_version_regex.is_match(version) if !valid_version_regex.is_match(version)
|| !valid_versions.contains(&version.as_str()) || !valid_versions.contains(&version.as_str())
{ {
return Err(format!( return Err(Error::UnsupportedVersion(opml.version));
"Unsupported OPML version detected: {}",
opml.version
));
} }
// SPEC: A `<body>` contains one or more `<outline>` elements. // SPEC: A `<body>` contains one or more `<outline>` elements.
if opml.body.outlines.is_empty() { if opml.body.outlines.is_empty() {
return Err("OPML body has no outlines.".to_string()); return Err(Error::BodyHasNoOutlines);
} }
Ok(opml) Ok(opml)
@ -172,13 +177,8 @@ impl OPML {
/// let expected = r#"<opml version="2.0"><head/><body/></opml>"#; /// let expected = r#"<opml version="2.0"><head/><body/></opml>"#;
/// assert_eq!(xml, expected); /// assert_eq!(xml, expected);
/// ``` /// ```
pub fn to_xml(&self) -> Result<String, String> { pub fn to_xml(&self) -> Result<String, Error> {
let result: Result<String, XmlError> = self.to_string(); Ok(self.to_string()?)
match result {
Ok(value) => Ok(value),
Err(err) => Err(format!("XML writing error: {:#?}", err)),
}
} }
} }

View File

@ -1,6 +1,5 @@
use std::fs::read_to_string as read;
use opml::*; use opml::*;
use std::fs::read_to_string as read;
#[test] #[test]
#[should_panic] #[should_panic]
@ -10,15 +9,15 @@ fn test_invalid_xml() {
} }
#[test] #[test]
#[should_panic(expected = "Unsupported OPML version detected: invalid")]
fn test_invalid_opml_version() { fn test_invalid_opml_version() {
let sample = read("tests/samples/invalid_opml_version.opml").unwrap(); let sample = read("tests/samples/invalid_opml_version.opml").unwrap();
OPML::new(sample.as_str()).unwrap(); let res = OPML::new(sample.as_str());
assert!(matches!(res, Err(Error::UnsupportedVersion(e)) if e == "invalid"));
} }
#[test] #[test]
#[should_panic(expected = "OPML body has no outlines.")]
fn test_invalid_opml_no_outlines() { fn test_invalid_opml_no_outlines() {
let sample = read("tests/samples/invalid_opml_no_outlines.opml").unwrap(); let sample = read("tests/samples/invalid_opml_no_outlines.opml").unwrap();
OPML::new(sample.as_str()).unwrap(); let res = OPML::new(sample.as_str());
assert!(matches!(res, Err(Error::BodyHasNoOutlines)));
} }