use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{CacheScope, ConflictKey, PromptProvenance, PromptSegmentId, TrustLevel};
pub const MAX_SEGMENT_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BudgetBehavior {
Required,
Truncate,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictMode {
Protected,
Replaceable,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConflictClaim {
key: ConflictKey,
mode: ConflictMode,
}
impl ConflictClaim {
#[must_use]
pub const fn new(key: ConflictKey, mode: ConflictMode) -> Self {
Self { key, mode }
}
#[must_use]
pub const fn key(&self) -> &ConflictKey {
&self.key
}
#[must_use]
pub const fn mode(&self) -> ConflictMode {
self.mode
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptSegment {
id: PromptSegmentId,
content: String,
provenance: PromptProvenance,
trust: TrustLevel,
cache_scope: CacheScope,
#[serde(skip_serializing_if = "Option::is_none")]
conflict: Option<ConflictClaim>,
budget_behavior: BudgetBehavior,
}
impl PromptSegment {
pub fn new(
id: PromptSegmentId,
content: impl Into<String>,
provenance: PromptProvenance,
trust: TrustLevel,
cache_scope: CacheScope,
budget_behavior: BudgetBehavior,
) -> Result<Self, SegmentError> {
let content = content.into();
validate_content(&content)?;
Ok(Self {
id,
content,
provenance,
trust,
cache_scope,
conflict: None,
budget_behavior,
})
}
#[must_use]
pub fn with_conflict(mut self, conflict: ConflictClaim) -> Self {
self.conflict = Some(conflict);
self
}
#[must_use]
pub const fn id(&self) -> &PromptSegmentId {
&self.id
}
#[must_use]
pub fn content(&self) -> &str {
&self.content
}
#[must_use]
pub const fn provenance(&self) -> &PromptProvenance {
&self.provenance
}
#[must_use]
pub const fn trust(&self) -> TrustLevel {
self.trust
}
#[must_use]
pub const fn cache_scope(&self) -> CacheScope {
self.cache_scope
}
#[must_use]
pub const fn conflict(&self) -> Option<&ConflictClaim> {
self.conflict.as_ref()
}
#[must_use]
pub const fn budget_behavior(&self) -> BudgetBehavior {
self.budget_behavior
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawPromptSegment {
id: PromptSegmentId,
content: String,
provenance: PromptProvenance,
trust: TrustLevel,
cache_scope: CacheScope,
#[serde(default)]
conflict: Option<ConflictClaim>,
budget_behavior: BudgetBehavior,
}
impl<'de> Deserialize<'de> for PromptSegment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawPromptSegment::deserialize(deserializer)?;
let mut segment = Self::new(
raw.id,
raw.content,
raw.provenance,
raw.trust,
raw.cache_scope,
raw.budget_behavior,
)
.map_err(serde::de::Error::custom)?;
segment.conflict = raw.conflict;
Ok(segment)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("prompt segment content is invalid")]
pub struct SegmentError;
fn validate_content(content: &str) -> Result<(), SegmentError> {
if content.is_empty() || content.len() > MAX_SEGMENT_BYTES || content.contains('\0') {
Err(SegmentError)
} else {
Ok(())
}
}