use nutype::nutype;
#[allow(unused_imports)] use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tracing::warn;
#[nutype(
validate(not_empty, len_char_max = 1000),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct UserAgent(String);
#[nutype(
validate(predicate = |s| std::net::IpAddr::from_str(s).is_ok()),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct IpAddress(String);
#[nutype(
validate(
not_empty,
len_char_max = 100,
regex = r"^[a-zA-Z0-9][a-zA-Z0-9:._-]*$"
),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct Tag(String);
#[nutype(
validate(not_empty, len_char_max = 1000),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct TestCaseDescription(String);
#[nutype(
validate(
not_empty,
len_char_max = 10485760,
predicate = |s| {
if s.len() > 100_000 {
warn!(
"Prompt template exceeds soft limit of 100k chars (actual: {} chars)",
s.len()
);
}
true // Always valid, just log warning
}
),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct PromptTemplate(String);
#[nutype(
validate(not_empty, len_char_max = 1000),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct Pattern(String);
#[nutype(
validate(
len_char_max = 10485760,
predicate = |s| {
if s.len() > 100_000 {
warn!(
"Response text exceeds soft limit of 100k chars (actual: {} chars, ~{} tokens)",
s.len(),
s.len() / 4
);
}
true // Always valid, just log warning
}
),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct ResponseText(String);
#[nutype(
validate(not_empty, len_char_max = 500),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct AssertionDescription(String);
#[nutype(
validate(not_empty, len_char_max = 5000),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct ErrorMessage(String);
#[nutype(
validate(not_empty, len_char_max = 200),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct ModelId(String);
#[nutype(
validate(
not_empty,
len_char_max = 10485760,
predicate = |s| {
if s.len() > 100_000 {
warn!(
"Prompt exceeds soft limit of 100k chars (actual: {} chars, ~{} tokens)",
s.len(),
s.len() / 4
);
}
true // Always valid, just log warning
}
),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct Prompt(String);
#[nutype(
validate(not_empty, len_char_max = 100),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct FinishReason(String);
#[nutype(
validate(less_or_equal = 1000000),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct TokenCount(u32);
#[nutype(
validate(less_or_equal = 300000), // 5 minutes max
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct Latency(u64);
#[nutype(derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
AsRef,
Display
))]
pub struct RequestCount(u64);
impl RequestCount {
pub fn increment(self) -> Self {
Self::new(self.as_ref().saturating_add(1))
}
}
#[nutype(
validate(not_empty, len_char_max = 100),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct FieldName(String);
#[nutype(
validate(not_empty, len_char_max = 200),
derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Display
)
)]
pub struct ResourceId(String);
#[nutype(
validate(not_empty, len_char_max = 1000),
derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRef, Display)
)]
pub struct ChangeReason(String);
#[nutype(derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize))]
pub struct LlmParameters(serde_json::Value);
#[nutype(derive(Debug, Clone, PartialEq, Serialize, Deserialize))]
pub struct MetadataAssertions(serde_json::Value);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_agent_validation() {
assert!(UserAgent::try_new("Mozilla/5.0".to_string()).is_ok());
assert!(UserAgent::try_new("".to_string()).is_err());
assert!(UserAgent::try_new("a".repeat(1001)).is_err());
}
#[test]
fn test_ip_address_validation() {
assert!(IpAddress::try_new("192.168.1.1".to_string()).is_ok());
assert!(IpAddress::try_new("10.0.0.0".to_string()).is_ok());
assert!(IpAddress::try_new("255.255.255.255".to_string()).is_ok());
assert!(IpAddress::try_new("2001:0db8:85a3:0000:0000:8a2e:0370:7334".to_string()).is_ok());
assert!(IpAddress::try_new("::1".to_string()).is_ok());
assert!(IpAddress::try_new("fe80::1".to_string()).is_ok());
assert!(IpAddress::try_new("256.1.1.1".to_string()).is_err());
assert!(IpAddress::try_new("192.168.1".to_string()).is_err());
assert!(IpAddress::try_new("not-an-ip".to_string()).is_err());
assert!(IpAddress::try_new("".to_string()).is_err());
}
#[test]
fn test_tag_validation() {
assert!(Tag::try_new("production".to_string()).is_ok());
assert!(Tag::try_new("api:v2".to_string()).is_ok());
assert!(Tag::try_new("test-case_1".to_string()).is_ok());
assert!(Tag::try_new("feature.enabled".to_string()).is_ok());
assert!(Tag::try_new("".to_string()).is_err());
assert!(Tag::try_new("-invalid".to_string()).is_err());
assert!(Tag::try_new("invalid ".to_string()).is_err());
assert!(Tag::try_new("a".repeat(101)).is_err());
}
#[test]
fn test_prompt_template_validation() {
assert!(PromptTemplate::try_new("Hello {name}!".to_string()).is_ok());
assert!(PromptTemplate::try_new("".to_string()).is_err());
assert!(PromptTemplate::try_new("a".repeat(1_000_000)).is_ok());
assert!(PromptTemplate::try_new("a".repeat(10_485_761)).is_err());
}
#[test]
fn test_pattern_validation() {
assert!(Pattern::try_new("expected output".to_string()).is_ok());
assert!(Pattern::try_new("".to_string()).is_err());
assert!(Pattern::try_new("a".repeat(1001)).is_err());
}
#[test]
fn test_soft_limit_warnings() {
let large_prompt = Prompt::try_new("a".repeat(200_000)).unwrap();
assert_eq!(large_prompt.as_ref().len(), 200_000);
let large_response = ResponseText::try_new("b".repeat(200_000)).unwrap();
assert_eq!(large_response.as_ref().len(), 200_000);
let large_template = PromptTemplate::try_new("c".repeat(200_000)).unwrap();
assert_eq!(large_template.as_ref().len(), 200_000);
}
}