use crate::{PaykitError, PrivateMessageKind, Result};
pub(crate) fn validate_uuid_v4(value: String, label: &'static str) -> Result<String> {
let uuid = uuid::Uuid::try_parse(&value)
.map_err(|_| PaykitError::Validation(format!("{label} must be a UUID v4 string")))?;
if uuid.get_version_num() != 4 || uuid.get_variant() != uuid::Variant::RFC4122 {
return Err(PaykitError::Validation(format!(
"{label} must be an RFC4122 UUID v4 string"
)));
}
Ok(uuid.hyphenated().to_string())
}
pub(crate) fn validate_decimal_text(value: &str, label: impl std::fmt::Display) -> Result<()> {
let mut seen_dot = false;
let mut seen_digit = false;
for byte in value.bytes() {
match byte {
b'.' if !seen_dot => seen_dot = true,
b'0'..=b'9' => seen_digit = true,
_ => {
return Err(PaykitError::Validation(format!(
"{label} must be a decimal string"
)))
}
}
}
if !seen_digit {
return Err(PaykitError::Validation(format!(
"{label} must contain at least one digit"
)));
}
Ok(())
}
pub(crate) fn validate_asset_text(value: &str, label: &str) -> Result<()> {
if value.is_empty() {
return Err(PaykitError::Validation(format!(
"{label} must not be empty"
)));
}
if value.chars().any(char::is_control) {
return Err(PaykitError::Validation(format!(
"{label} must not contain control characters"
)));
}
Ok(())
}
pub(crate) fn invalid_data(
context: impl Into<String>,
source: Option<anyhow::Error>,
) -> PaykitError {
PaykitError::InvalidData {
context: context.into(),
source,
}
}
pub(crate) fn invalid_plaintext_json(context: &'static str) -> PaykitError {
PaykitError::InvalidData {
context: context.into(),
source: None,
}
}
pub(crate) fn invalid_wire(err: PaykitError, label: &'static str) -> PaykitError {
match err {
PaykitError::Validation(msg) => invalid_data(format!("{label}: {msg}"), None),
other => other,
}
}
pub(crate) fn parse_utc_timestamp(
value: &str,
field: &str,
) -> Result<chrono::DateTime<chrono::FixedOffset>> {
if !value.ends_with('Z') {
return Err(PaykitError::Validation(format!(
"{field} must be an RFC3339 UTC timestamp using the Z suffix"
)));
}
chrono::DateTime::parse_from_rfc3339(value).map_err(|err| {
PaykitError::Validation(format!("{field} must be a valid RFC3339 timestamp: {err}"))
})
}
pub(crate) fn validate_wire_version_kind(
version: u8,
kind: &str,
expected: PrivateMessageKind,
label: &'static str,
) -> Result<()> {
validate_wire_version_kind_str(version, kind, expected.as_str(), label)
}
pub(crate) fn validate_wire_version_kind_str(
version: u8,
kind: &str,
expected_kind: &str,
label: &'static str,
) -> Result<()> {
if version != 1 || kind != expected_kind {
return Err(invalid_data(
format!("unsupported {label} version/kind"),
None,
));
}
Ok(())
}
pub(crate) fn validate_outgoing_version_kind(
version: u8,
kind: PrivateMessageKind,
expected: PrivateMessageKind,
label: &'static str,
) -> Result<()> {
if version != 1 || kind != expected {
return Err(PaykitError::Validation(format!(
"{label} must use version 1 and kind {}",
expected.as_str()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_decimal_text_accepts_digits_with_one_optional_dot() {
for accepted in [".5", "10.", "0001.2300", "0"] {
assert!(validate_decimal_text(accepted, "test amount").is_ok());
}
for rejected in ["", ".", "-1", "+1", "1e2", "1,000", "1.2.3", " 1"] {
assert!(validate_decimal_text(rejected, "test amount").is_err());
}
}
}