1use crate::text::{normalize_optional_text, normalize_optional_text_ref};
2use crate::{CoreError, Result};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::fmt;
5use std::str::FromStr;
6
7const CUSTOM_PREFIX: &str = "custom:";
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum SourceKind {
12 Document,
14 Log,
16 Api,
18 Human,
20 Ai,
22 Code,
24 External,
26 Custom(String),
28}
29
30impl SourceKind {
31 pub fn custom(extension: impl Into<String>) -> Result<Self> {
33 let raw = extension.into();
34 let normalized = raw.trim().to_owned();
35
36 if normalized.is_empty() {
37 return Err(CoreError::invalid_source_kind(
38 raw,
39 "custom source kind extension must not be empty after trimming",
40 ));
41 }
42
43 Ok(Self::Custom(normalized))
44 }
45
46 pub fn is_custom(&self) -> bool {
48 matches!(self, Self::Custom(_))
49 }
50
51 pub fn serialized_value(&self) -> String {
55 self.try_serialized_value()
56 .unwrap_or_else(|_| CUSTOM_PREFIX.to_owned())
57 }
58
59 pub fn try_serialized_value(&self) -> Result<String> {
61 match self {
62 Self::Document => Ok("document".to_owned()),
63 Self::Log => Ok("log".to_owned()),
64 Self::Api => Ok("api".to_owned()),
65 Self::Human => Ok("human".to_owned()),
66 Self::Ai => Ok("ai".to_owned()),
67 Self::Code => Ok("code".to_owned()),
68 Self::External => Ok("external".to_owned()),
69 Self::Custom(extension) => {
70 let custom = Self::custom(extension.clone())?;
71 let Self::Custom(normalized) = custom else {
72 unreachable!("SourceKind::custom always returns a custom source kind");
73 };
74 Ok(format!("{CUSTOM_PREFIX}{normalized}"))
75 }
76 }
77 }
78}
79
80impl FromStr for SourceKind {
81 type Err = CoreError;
82
83 fn from_str(value: &str) -> Result<Self> {
84 match value {
85 "document" => Ok(Self::Document),
86 "log" => Ok(Self::Log),
87 "api" => Ok(Self::Api),
88 "human" => Ok(Self::Human),
89 "ai" => Ok(Self::Ai),
90 "code" => Ok(Self::Code),
91 "external" => Ok(Self::External),
92 custom if custom.starts_with(CUSTOM_PREFIX) => {
93 Self::custom(&custom[CUSTOM_PREFIX.len()..])
94 }
95 unknown => Err(CoreError::invalid_source_kind(
96 unknown,
97 "expected document, log, api, human, ai, code, external, or custom:<extension>",
98 )),
99 }
100 }
101}
102
103impl Serialize for SourceKind {
104 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
105 where
106 S: Serializer,
107 {
108 let value = self
109 .try_serialized_value()
110 .map_err(serde::ser::Error::custom)?;
111 serializer.serialize_str(&value)
112 }
113}
114
115impl<'de> Deserialize<'de> for SourceKind {
116 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
117 where
118 D: Deserializer<'de>,
119 {
120 let value = String::deserialize(deserializer)?;
121 Self::from_str(&value).map_err(serde::de::Error::custom)
122 }
123}
124
125impl fmt::Display for SourceKind {
126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127 formatter.write_str(&self.serialized_value())
128 }
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct SourceRef {
134 pub kind: SourceKind,
136 pub uri: Option<String>,
138 pub title: Option<String>,
140 pub captured_at: Option<String>,
142 pub source_local_id: Option<String>,
144}
145
146impl SourceRef {
147 pub fn new(kind: SourceKind) -> Self {
149 Self {
150 kind,
151 uri: None,
152 title: None,
153 captured_at: None,
154 source_local_id: None,
155 }
156 }
157
158 pub fn with_uri(mut self, uri: impl Into<String>) -> Result<Self> {
160 self.uri = normalize_optional_text("uri", Some(uri.into()))?;
161 Ok(self)
162 }
163
164 pub fn with_title(mut self, title: impl Into<String>) -> Result<Self> {
166 self.title = normalize_optional_text("title", Some(title.into()))?;
167 Ok(self)
168 }
169
170 pub fn with_captured_at(mut self, captured_at: impl Into<String>) -> Result<Self> {
172 self.captured_at = normalize_optional_text("captured_at", Some(captured_at.into()))?;
173 Ok(self)
174 }
175
176 pub fn with_source_local_id(mut self, source_local_id: impl Into<String>) -> Result<Self> {
178 self.source_local_id =
179 normalize_optional_text("source_local_id", Some(source_local_id.into()))?;
180 Ok(self)
181 }
182
183 pub fn validate(&self) -> Result<()> {
185 self.to_wire().map(|_| ())
186 }
187
188 fn from_wire(wire: SourceRefWire) -> Result<Self> {
189 Ok(Self {
190 kind: wire.kind,
191 uri: normalize_optional_text("uri", wire.uri)?,
192 title: normalize_optional_text("title", wire.title)?,
193 captured_at: normalize_optional_text("captured_at", wire.captured_at)?,
194 source_local_id: normalize_optional_text("source_local_id", wire.source_local_id)?,
195 })
196 }
197
198 fn to_wire(&self) -> Result<SourceRefWire> {
199 self.kind.try_serialized_value()?;
200
201 Ok(SourceRefWire {
202 kind: self.kind.clone(),
203 uri: normalize_optional_text_ref("uri", self.uri.as_ref())?,
204 title: normalize_optional_text_ref("title", self.title.as_ref())?,
205 captured_at: normalize_optional_text_ref("captured_at", self.captured_at.as_ref())?,
206 source_local_id: normalize_optional_text_ref(
207 "source_local_id",
208 self.source_local_id.as_ref(),
209 )?,
210 })
211 }
212}
213
214impl Serialize for SourceRef {
215 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
216 where
217 S: Serializer,
218 {
219 self.to_wire()
220 .map_err(serde::ser::Error::custom)?
221 .serialize(serializer)
222 }
223}
224
225impl<'de> Deserialize<'de> for SourceRef {
226 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
227 where
228 D: Deserializer<'de>,
229 {
230 let wire = SourceRefWire::deserialize(deserializer)?;
231 Self::from_wire(wire).map_err(serde::de::Error::custom)
232 }
233}
234
235#[derive(Deserialize, Serialize)]
236#[serde(deny_unknown_fields)]
237struct SourceRefWire {
238 kind: SourceKind,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 uri: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 title: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 captured_at: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 source_local_id: Option<String>,
247}