1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4use crate::{CacheScope, ConflictKey, PromptProvenance, PromptSegmentId, TrustLevel};
5
6pub const MAX_SEGMENT_BYTES: usize = 1024 * 1024;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum BudgetBehavior {
13 Required,
15 Truncate,
17 Omit,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ConflictMode {
25 Protected,
27 Replaceable,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct ConflictClaim {
35 key: ConflictKey,
36 mode: ConflictMode,
37}
38
39impl ConflictClaim {
40 #[must_use]
42 pub const fn new(key: ConflictKey, mode: ConflictMode) -> Self {
43 Self { key, mode }
44 }
45 #[must_use]
47 pub const fn key(&self) -> &ConflictKey {
48 &self.key
49 }
50 #[must_use]
52 pub const fn mode(&self) -> ConflictMode {
53 self.mode
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PromptSegment {
61 id: PromptSegmentId,
62 content: String,
63 provenance: PromptProvenance,
64 trust: TrustLevel,
65 cache_scope: CacheScope,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 conflict: Option<ConflictClaim>,
68 budget_behavior: BudgetBehavior,
69}
70
71impl PromptSegment {
72 pub fn new(
78 id: PromptSegmentId,
79 content: impl Into<String>,
80 provenance: PromptProvenance,
81 trust: TrustLevel,
82 cache_scope: CacheScope,
83 budget_behavior: BudgetBehavior,
84 ) -> Result<Self, SegmentError> {
85 let content = content.into();
86 validate_content(&content)?;
87 Ok(Self {
88 id,
89 content,
90 provenance,
91 trust,
92 cache_scope,
93 conflict: None,
94 budget_behavior,
95 })
96 }
97
98 #[must_use]
100 pub fn with_conflict(mut self, conflict: ConflictClaim) -> Self {
101 self.conflict = Some(conflict);
102 self
103 }
104
105 #[must_use]
107 pub const fn id(&self) -> &PromptSegmentId {
108 &self.id
109 }
110 #[must_use]
112 pub fn content(&self) -> &str {
113 &self.content
114 }
115 #[must_use]
117 pub const fn provenance(&self) -> &PromptProvenance {
118 &self.provenance
119 }
120 #[must_use]
122 pub const fn trust(&self) -> TrustLevel {
123 self.trust
124 }
125 #[must_use]
127 pub const fn cache_scope(&self) -> CacheScope {
128 self.cache_scope
129 }
130 #[must_use]
132 pub const fn conflict(&self) -> Option<&ConflictClaim> {
133 self.conflict.as_ref()
134 }
135 #[must_use]
137 pub const fn budget_behavior(&self) -> BudgetBehavior {
138 self.budget_behavior
139 }
140}
141
142#[derive(Deserialize)]
143#[serde(rename_all = "camelCase")]
144struct RawPromptSegment {
145 id: PromptSegmentId,
146 content: String,
147 provenance: PromptProvenance,
148 trust: TrustLevel,
149 cache_scope: CacheScope,
150 #[serde(default)]
151 conflict: Option<ConflictClaim>,
152 budget_behavior: BudgetBehavior,
153}
154
155impl<'de> Deserialize<'de> for PromptSegment {
156 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157 where
158 D: serde::Deserializer<'de>,
159 {
160 let raw = RawPromptSegment::deserialize(deserializer)?;
161 let mut segment = Self::new(
162 raw.id,
163 raw.content,
164 raw.provenance,
165 raw.trust,
166 raw.cache_scope,
167 raw.budget_behavior,
168 )
169 .map_err(serde::de::Error::custom)?;
170 segment.conflict = raw.conflict;
171 Ok(segment)
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
177#[error("prompt segment content is invalid")]
178pub struct SegmentError;
179
180fn validate_content(content: &str) -> Result<(), SegmentError> {
181 if content.is_empty() || content.len() > MAX_SEGMENT_BYTES || content.contains('\0') {
182 Err(SegmentError)
183 } else {
184 Ok(())
185 }
186}