use crate::ascii::response::{
AnyResponse, Flag, Kind, Reply, Response, ResponseWithStatus, ResponseWithWarning,
SpecificResponse, Status, Target, Warning,
};
#[allow(clippy::wildcard_imports)]
use crate::error::*;
pub trait Check<R: Response>: private::Sealed<R> {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>>;
}
impl<R: Response, F: Fn(R) -> Result<R, AsciiCheckError<R>>> Check<R> for F {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
(self)(response)
}
}
impl<R: Response> Check<R> for &dyn Check<R> {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
(*self).check(response)
}
}
#[derive(Debug)]
pub struct AnyResponseCheck<K, R>(K, std::marker::PhantomData<R>);
impl<K, R> Check<AnyResponse> for AnyResponseCheck<K, R>
where
K: Check<R>,
R: SpecificResponse,
{
fn check(
&self,
any_response: AnyResponse,
) -> Result<AnyResponse, AsciiCheckError<AnyResponse>> {
match R::try_from(any_response) {
Ok(response) => self.0.check(response).map(Into::into).map_err(Into::into),
Err(any_response) => Ok(any_response),
}
}
}
impl<K> Check<AnyResponse> for AnyResponseCheck<K, AnyResponse>
where
K: Check<AnyResponse>,
{
fn check(
&self,
any_response: AnyResponse,
) -> Result<AnyResponse, AsciiCheckError<AnyResponse>> {
self.0.check(any_response)
}
}
impl<K, R> From<K> for AnyResponseCheck<K, R>
where
R: Response,
K: Check<R>,
{
fn from(other: K) -> AnyResponseCheck<K, R> {
AnyResponseCheck(other, std::marker::PhantomData)
}
}
mod private {
#[allow(clippy::wildcard_imports)]
use super::*;
pub trait Sealed<R: Response> {}
impl<R: Response> Sealed<R> for &dyn Check<R> {}
impl<R: Response, F: Fn(R) -> Result<R, AsciiCheckError<R>>> Sealed<R> for F {}
impl<K: Check<R>, R: Response> Sealed<AnyResponse> for AnyResponseCheck<K, R> {}
}
#[derive(Debug)]
#[repr(transparent)]
#[must_use]
pub struct NotChecked<R>(R);
impl<R: Response> NotChecked<R> {
pub(crate) fn new(response: R) -> Self {
NotChecked(response)
}
pub(crate) fn into_inner(self) -> R {
self.0
}
pub fn check(self, checker: impl Check<R>) -> Result<R, AsciiCheckError<R>> {
checker.check(self.0)
}
pub fn check_minimal(self) -> Result<R, AsciiCheckError<R>> {
minimal().check(self.0)
}
pub fn check_strict(self) -> Result<R, AsciiCheckError<R>> {
strict().check(self.0)
}
pub fn target(&self) -> Target {
self.0.target()
}
pub fn id(&self) -> Option<u8> {
self.0.id()
}
}
impl NotChecked<Reply> {
pub fn flag_ok(self) -> Result<Reply, AsciiCheckError<Reply>> {
flag_ok().check(self.0)
}
pub fn flag_ok_and(self, checker: impl Check<Reply>) -> Result<Reply, AsciiCheckError<Reply>> {
flag_ok_and(checker).check(self.0)
}
}
impl NotChecked<AnyResponse> {
pub fn kind(&self) -> Kind {
self.0.kind()
}
}
pub fn warning_is<R, W>(warning: W) -> impl Check<R>
where
R: ResponseWithWarning,
Warning: std::convert::TryFrom<W>,
{
use std::convert::TryFrom as _;
let warning = Warning::try_from(warning).unwrap_or_else(|_| panic!("Invalid warning"));
move |response: R| {
if response.warning() == warning {
Ok(response)
} else {
Err(
AsciiCheckWarningError::new(format!("expected {warning} warning flag"), response)
.into(),
)
}
}
}
pub fn warning_in<R: ResponseWithWarning, L: WarningList>(warnings: L) -> impl Check<R> {
move |response: R| {
if warnings.contains(response.warning()) {
Ok(response)
} else {
use std::fmt::Write as _;
let mut msg = String::new();
msg.write_str("expected one of ").unwrap();
warnings.write_fmt_to_str(&mut msg);
msg.write_str(" warning flag(s)").unwrap();
Err(AsciiCheckWarningError::new(msg, response).into())
}
}
}
pub fn warning_below_fault<R: ResponseWithWarning>() -> impl Check<R> {
|response: R| {
if response.warning().is_fault() {
Err(
AsciiCheckWarningError::new("expected warning below fault (F) level", response)
.into(),
)
} else {
Ok(response)
}
}
}
pub fn warning_below_warning<R: ResponseWithWarning>() -> impl Check<R> {
|response: R| {
let warning = response.warning();
if !warning.is_fault() && !warning.is_warning() {
Ok(response)
} else {
Err(
AsciiCheckWarningError::new("expected warning below warning (W) level", response)
.into(),
)
}
}
}
pub fn warning_is_none<R: ResponseWithWarning>() -> impl Check<R> {
|response: R| {
if response.warning().is_none() {
Ok(response)
} else {
Err(AsciiCheckWarningError::new("expected no warning (--)", response).into())
}
}
}
pub fn status_is<R: ResponseWithStatus>(status: Status) -> impl Check<R> {
move |response: R| {
if response.status() == status {
Ok(response)
} else {
Err(AsciiCheckStatusError::new(status, response).into())
}
}
}
pub fn status_idle<R: ResponseWithStatus>() -> impl Check<R> {
status_is(Status::Idle)
}
pub fn status_busy<R: ResponseWithStatus>() -> impl Check<R> {
status_is(Status::Busy)
}
pub fn flag_is(flag: Flag) -> impl Check<Reply> {
move |response: Reply| {
if response.flag() == flag {
Ok(response)
} else {
Err(AsciiCheckFlagError::new(flag, response).into())
}
}
}
pub fn flag_ok() -> impl Check<Reply> {
flag_is(Flag::Ok)
}
pub fn flag_rj() -> impl Check<Reply> {
flag_is(Flag::Rj)
}
pub fn flag_ok_and(check: impl Check<Reply>) -> impl Check<Reply> {
all((flag_ok(), check))
}
pub fn parsed_data_is<
R: Response,
T: std::str::FromStr + std::cmp::PartialEq<T> + std::fmt::Debug,
>(
value: T,
) -> impl Check<R> {
move |response: R| {
if let Ok(parsed) = response.data().parse::<T>() {
if parsed == value {
Ok(response)
} else {
Err(AsciiCheckDataError::new(format!("expected data {value:?}"), response).into())
}
} else {
Err(AsciiCheckDataError::new("could not parse data as expected type", response).into())
}
}
}
pub fn all<R: Response, C: CheckAll<R>>(checks: C) -> impl Check<R> {
move |response: R| checks.check(response)
}
pub fn strict<R: Response>() -> impl Check<R> {
R::strict()
}
pub fn minimal<R: Response>() -> impl Check<R> {
R::minimal()
}
pub fn unchecked<R: Response>() -> impl Check<R> {
|response: R| Ok(response)
}
pub fn predicate<R: Response, P: Fn(&R) -> bool>(predicate: P) -> impl Check<R> {
move |response: R| {
if predicate(&response) {
Ok(response)
} else {
Err(AsciiCheckCustomError::unknown(response).into())
}
}
}
pub trait CheckAll<R: Response> {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>>;
}
impl<R: Response, A: Check<R>> CheckAll<R> for (A,) {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
self.0.check(response)
}
}
impl<R: Response, A: Check<R>, B: Check<R>> CheckAll<R> for (A, B) {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
let response = self.0.check(response)?;
let response = self.1.check(response)?;
Ok(response)
}
}
impl<R: Response, A: Check<R>, B: Check<R>, C: Check<R>> CheckAll<R> for (A, B, C) {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
let response = self.0.check(response)?;
let response = self.1.check(response)?;
let response = self.2.check(response)?;
Ok(response)
}
}
impl<R: Response, A: Check<R>, B: Check<R>, C: Check<R>, D: Check<R>> CheckAll<R> for (A, B, C, D) {
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
let response = self.0.check(response)?;
let response = self.1.check(response)?;
let response = self.2.check(response)?;
let response = self.3.check(response)?;
Ok(response)
}
}
impl<R: Response, A: Check<R>, B: Check<R>, C: Check<R>, D: Check<R>, E: Check<R>> CheckAll<R>
for (A, B, C, D, E)
{
fn check(&self, response: R) -> Result<R, AsciiCheckError<R>> {
let response = self.0.check(response)?;
let response = self.1.check(response)?;
let response = self.2.check(response)?;
let response = self.3.check(response)?;
let response = self.4.check(response)?;
Ok(response)
}
}
pub trait WarningList {
fn contains(&self, warning: Warning) -> bool;
fn write_fmt_to_str(&self, s: &mut String);
}
impl<A> WarningList for (A,)
where
A: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
warning == self.0
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
s.write_fmt(format_args!("{}", String::from_utf8_lossy(self.0.as_ref())))
.unwrap();
}
}
impl<A, B> WarningList for (A, B)
where
A: AsRef<[u8]>,
B: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
warning == self.0 || warning == self.1
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
s.write_fmt(format_args!(
"{} or {}",
String::from_utf8_lossy(self.0.as_ref()),
String::from_utf8_lossy(self.1.as_ref())
))
.unwrap();
}
}
impl<A, B, C> WarningList for (A, B, C)
where
A: AsRef<[u8]>,
B: AsRef<[u8]>,
C: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
warning == self.0 || warning == self.1 || warning == self.2
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
s.write_fmt(format_args!(
"{}, {}, or {}",
String::from_utf8_lossy(self.0.as_ref()),
String::from_utf8_lossy(self.1.as_ref()),
String::from_utf8_lossy(self.2.as_ref())
))
.unwrap();
}
}
impl<A, B, C, D> WarningList for (A, B, C, D)
where
A: AsRef<[u8]>,
B: AsRef<[u8]>,
C: AsRef<[u8]>,
D: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
warning == self.0 || warning == self.1 || warning == self.2 || warning == self.3
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
s.write_fmt(format_args!(
"{}, {}, {}, or {}",
String::from_utf8_lossy(self.0.as_ref()),
String::from_utf8_lossy(self.1.as_ref()),
String::from_utf8_lossy(self.2.as_ref()),
String::from_utf8_lossy(self.3.as_ref())
))
.unwrap();
}
}
impl<A, B, C, D, E> WarningList for (A, B, C, D, E)
where
A: AsRef<[u8]>,
B: AsRef<[u8]>,
C: AsRef<[u8]>,
D: AsRef<[u8]>,
E: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
warning == self.0
|| warning == self.1
|| warning == self.2
|| warning == self.3
|| warning == self.4
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
s.write_fmt(format_args!(
"{}, {}, {}, {}, or {}",
String::from_utf8_lossy(self.0.as_ref()),
String::from_utf8_lossy(self.1.as_ref()),
String::from_utf8_lossy(self.2.as_ref()),
String::from_utf8_lossy(self.3.as_ref()),
String::from_utf8_lossy(self.4.as_ref())
))
.unwrap();
}
}
impl<A> WarningList for [A]
where
A: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
for item in self {
if warning == *item {
return true;
}
}
false
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
for (i, item) in self.iter().enumerate() {
let prefix = match i {
0 => "",
i if i == self.len() - 1 => ", or ",
_ => ", ",
};
s.write_fmt(format_args!(
"{}{}",
prefix,
String::from_utf8_lossy(item.as_ref())
))
.unwrap();
}
}
}
impl<A, const SIZE: usize> WarningList for [A; SIZE]
where
A: AsRef<[u8]>,
{
fn contains(&self, warning: Warning) -> bool {
for item in self {
if warning == *item {
return true;
}
}
false
}
fn write_fmt_to_str(&self, s: &mut String) {
use std::fmt::Write as _;
for (i, item) in self.iter().enumerate() {
let prefix = match i {
0 => "",
i if i == self.len() - 1 => ", or ",
_ => ", ",
};
s.write_fmt(format_args!(
"{}{}",
prefix,
String::from_utf8_lossy(item.as_ref())
))
.unwrap();
}
}
}
impl<T: WarningList> WarningList for &T {
fn contains(&self, warning: Warning) -> bool {
(*self).contains(warning)
}
fn write_fmt_to_str(&self, s: &mut String) {
(*self).write_fmt_to_str(s);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ascii::{packet::Packet, response::Reply};
#[test]
fn check_reply() {
struct Case<'a> {
reply: Reply,
checker: &'a dyn Check<Reply>,
expected: Result<(), AsciiCheckError<Reply>>,
}
let ok_busy_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 OK BUSY -- 0\r\n").unwrap()).unwrap();
let ok_idle_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 OK IDLE -- 0\r\n").unwrap()).unwrap();
let ok_idle_ff_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 OK IDLE FF 0\r\n").unwrap()).unwrap();
let ok_idle_wh_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 OK IDLE WH 0\r\n").unwrap()).unwrap();
let ok_idle_ni_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 OK IDLE NI 0\r\n").unwrap()).unwrap();
let rj_idle_reply =
Reply::try_from_packet(&Packet::new(b"@01 1 12 RJ IDLE -- BADCOMMAND\r\n").unwrap())
.unwrap();
let predicate_check = predicate(|reply: &Reply| {
if reply.flag() == Flag::Ok {
reply.status() == Status::Idle
} else {
reply.data() == "0"
}
});
let cases = &[
Case {
reply: ok_busy_reply.clone(),
checker: &flag_ok(),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &flag_is(Flag::Rj),
expected: Err(AsciiCheckFlagError::new(Flag::Rj, ok_busy_reply.clone()).into()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &flag_rj(),
expected: Err(AsciiCheckFlagError::new(Flag::Rj, ok_busy_reply.clone()).into()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &flag_ok_and(status_is(Status::Busy)),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &flag_ok_and(status_is(Status::Idle)),
expected: Err(
AsciiCheckStatusError::new(Status::Idle, ok_busy_reply.clone()).into(),
),
},
Case {
reply: ok_busy_reply.clone(),
checker: &unchecked(),
expected: Ok(()),
},
Case {
reply: rj_idle_reply.clone(),
checker: &unchecked(),
expected: Ok(()),
},
Case {
reply: ok_idle_reply.clone(),
checker: &strict(),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &strict(),
expected: Ok(()),
},
Case {
reply: ok_idle_ni_reply.clone(),
checker: &strict(),
expected: Err(AsciiCheckWarningError::new(
"expected no warning (--)",
ok_idle_ni_reply.clone(),
)
.into()),
},
Case {
reply: rj_idle_reply.clone(),
checker: &strict(),
expected: Err(AsciiCheckFlagError::new(Flag::Ok, rj_idle_reply.clone()).into()),
},
Case {
reply: ok_idle_reply.clone(),
checker: &minimal(),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &minimal(),
expected: Ok(()),
},
Case {
reply: ok_idle_ni_reply.clone(),
checker: &minimal(),
expected: Ok(()),
},
Case {
reply: ok_idle_wh_reply.clone(),
checker: &minimal(),
expected: Ok(()),
},
Case {
reply: ok_idle_ff_reply.clone(),
checker: &minimal(),
expected: Err(AsciiCheckWarningError::new(
"expected warning below fault (F) level",
ok_idle_ff_reply.clone(),
)
.into()),
},
Case {
reply: rj_idle_reply.clone(),
checker: &minimal(),
expected: Err(AsciiCheckFlagError::new(Flag::Ok, rj_idle_reply).into()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &warning_is("WR"),
expected: Err(AsciiCheckWarningError::new(
"expected WR warning flag",
ok_busy_reply.clone(),
)
.into()),
},
Case {
reply: ok_idle_wh_reply.clone(),
checker: &warning_is("WH"),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &warning_is_none(),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &warning_in(("WR", Warning::NONE)),
expected: Ok(()),
},
Case {
reply: ok_idle_ff_reply.clone(),
checker: &warning_in(("WR", Warning::NONE)),
expected: Err(AsciiCheckWarningError::new(
"expected one of WR or -- warning flag(s)",
ok_idle_ff_reply.clone(),
)
.into()),
},
Case {
reply: ok_idle_ff_reply.clone(),
checker: &warning_below_fault(),
expected: Err(AsciiCheckWarningError::new(
"expected warning below fault (F) level",
ok_idle_ff_reply,
)
.into()),
},
Case {
reply: ok_idle_wh_reply.clone(),
checker: &warning_below_fault(),
expected: Ok(()),
},
Case {
reply: ok_idle_wh_reply.clone(),
checker: &warning_below_warning(),
expected: Err(AsciiCheckWarningError::new(
"expected warning below warning (W) level",
ok_idle_wh_reply,
)
.into()),
},
Case {
reply: ok_idle_ni_reply,
checker: &warning_below_warning(),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &all((
flag_is(Flag::Ok),
warning_in(&["--"]),
status_is(Status::Busy),
)),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &parsed_data_is(0),
expected: Ok(()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &parsed_data_is(1),
expected: Err(
AsciiCheckDataError::new("expected data 1", ok_busy_reply.clone()).into(),
),
},
Case {
reply: ok_busy_reply.clone(),
checker: &parsed_data_is(std::num::NonZeroIsize::new(1).unwrap()),
expected: Err(AsciiCheckDataError::new(
"could not parse data as expected type",
ok_busy_reply.clone(),
)
.into()),
},
Case {
reply: ok_busy_reply.clone(),
checker: &predicate_check,
expected: Err(AsciiCheckCustomError::new("invalid response", ok_busy_reply).into()),
},
Case {
reply: ok_idle_reply,
checker: &predicate_check,
expected: Ok(()),
},
];
for (i, case) in cases.iter().enumerate() {
println!("case {}: {}", i, case.reply);
let actual = case.checker.check(case.reply.clone());
match &case.expected {
Ok(_) => assert!(actual.is_ok(), "unexpected error: {actual:?}"),
Err(expected_err) => {
let actual_err = actual.unwrap_err();
assert_eq!(*expected_err, actual_err);
}
}
}
}
#[test]
fn test_warning_list() {
warning_in::<Reply, _>((
Warning::from(b"FF"),
"WR",
b"RR",
"--".to_string(),
b"--".to_vec(),
));
}
}