use std::time::Duration;
use qubit_argument::{
ArgumentError,
ArgumentErrorKind,
ArgumentResult,
ArgumentResultExt,
ArgumentValue,
ComparisonConstraint,
DurationArgument,
FloatArgument,
NumericArgument,
OptionArgument,
StringArgument,
};
#[derive(Debug, PartialEq, Eq)]
enum DomainError {
InvalidArgument(ArgumentError),
}
impl From<ArgumentError> for DomainError {
fn from(error: ArgumentError) -> Self {
Self::InvalidArgument(error)
}
}
fn into_argument_error(error: DomainError) -> ArgumentError {
let DomainError::InvalidArgument(error) = error;
error
}
fn build_topic(name: String) -> Result<String, DomainError> {
let name = name.require_non_blank("name")?;
Ok(name)
}
#[test]
fn test_event_bus_topic_uses_domain_conversion_without_map_err() {
let name = String::from("orders.created");
assert_eq!(
build_topic(name).expect("non-blank topic name is valid"),
"orders.created",
);
let error = build_topic(String::from("\u{2003}"))
.expect_err("blank topic name must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "name");
assert_eq!(error.kind(), &ArgumentErrorKind::Blank);
}
struct ThreadPoolOptions {
maximum_pool_size: usize,
core_pool_size: usize,
queue_capacity: Option<usize>,
stack_size: Option<usize>,
keep_alive: Duration,
}
impl ThreadPoolOptions {
fn validate(&self) -> Result<(), DomainError> {
self.maximum_pool_size
.require_positive("maximum_pool_size")?;
self.core_pool_size
.require_at_most("core_pool_size", self.maximum_pool_size)?;
self.queue_capacity.validate_if_some(|capacity| {
(*capacity).require_positive("queue_capacity")?;
Ok(())
})?;
self.stack_size.validate_if_some(|stack_size| {
(*stack_size).require_positive("stack_size")?;
Ok(())
})?;
self.keep_alive.require_positive("keep_alive")?;
Ok(())
}
}
#[test]
fn test_thread_pool_builder_composes_argument_traits() {
let options = ThreadPoolOptions {
maximum_pool_size: 8,
core_pool_size: 4,
queue_capacity: Some(0),
stack_size: Some(2 * 1024 * 1024),
keep_alive: Duration::from_secs(30),
};
let error = options
.validate()
.expect_err("zero optional queue capacity must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "queue_capacity");
assert_eq!(
error.kind(),
&ArgumentErrorKind::Comparison {
actual: ArgumentValue::from(0_usize),
constraint: ComparisonConstraint::GreaterThan(ArgumentValue::from(
0_usize
),),
},
);
}
#[test]
fn test_thread_pool_builder_reports_duration_value() {
let options = ThreadPoolOptions {
maximum_pool_size: 8,
core_pool_size: 4,
queue_capacity: Some(32),
stack_size: None,
keep_alive: Duration::ZERO,
};
let error = options.validate().expect_err("zero keep-alive must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "keep_alive");
assert_eq!(
error.kind(),
&ArgumentErrorKind::Comparison {
actual: ArgumentValue::from(Duration::ZERO),
constraint: ComparisonConstraint::GreaterThan(ArgumentValue::from(
Duration::ZERO
),),
},
);
}
struct TimeoutOptions {
connect: Duration,
}
impl TimeoutOptions {
fn validate(&self) -> ArgumentResult<()> {
self.connect.require_positive("connect")?;
Ok(())
}
}
struct HttpOptions {
timeouts: TimeoutOptions,
}
impl HttpOptions {
fn validate(&self) -> Result<(), DomainError> {
self.timeouts.validate().with_path_prefix("timeouts")?;
Ok(())
}
}
#[test]
fn test_http_options_prefix_nested_error_path() {
let options = HttpOptions {
timeouts: TimeoutOptions {
connect: Duration::ZERO,
},
};
let error = options
.validate()
.expect_err("zero connection timeout must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "timeouts.connect");
assert!(matches!(error.kind(), ArgumentErrorKind::Comparison { .. },));
}
struct RetryDelayOptions {
initial: Duration,
max: Duration,
multiplier: f64,
}
impl RetryDelayOptions {
fn validate(&self) -> ArgumentResult<()> {
self.initial.require_positive("initial")?;
self.max.require_at_least("max", self.initial)?;
self.multiplier
.require_finite("multiplier")?
.require_greater_than("multiplier", 1.0)?;
Ok(())
}
}
struct RetryOptions {
delay: RetryDelayOptions,
}
impl RetryOptions {
fn validate(&self) -> Result<(), DomainError> {
self.delay.validate().with_path_prefix("delay")?;
Ok(())
}
}
#[test]
fn test_retry_options_require_finite_multiplier() {
let options = RetryOptions {
delay: RetryDelayOptions {
initial: Duration::from_millis(100),
max: Duration::from_secs(10),
multiplier: f64::INFINITY,
},
};
let error = options
.validate()
.expect_err("infinite retry multiplier must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "delay.multiplier");
assert_eq!(
error.kind(),
&ArgumentErrorKind::NotFinite {
actual: ArgumentValue::from(f64::INFINITY),
},
);
}
#[test]
fn test_retry_options_compare_duration_fields() {
let initial = Duration::from_secs(2);
let max = Duration::from_secs(1);
let options = RetryOptions {
delay: RetryDelayOptions {
initial,
max,
multiplier: 2.0,
},
};
let error = options
.validate()
.expect_err("maximum delay below initial delay must fail");
let error = into_argument_error(error);
assert_eq!(error.path().as_str(), "delay.max");
assert_eq!(
error.kind(),
&ArgumentErrorKind::Comparison {
actual: ArgumentValue::from(max),
constraint: ComparisonConstraint::AtLeast(ArgumentValue::from(
initial,
)),
},
);
}