Skip to main content

higher_graphen_core/
source.rs

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/// Category of source material behind an observation or inference.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum SourceKind {
12    /// Document source material.
13    Document,
14    /// Log source material.
15    Log,
16    /// API source material.
17    Api,
18    /// Human-provided source material.
19    Human,
20    /// AI-generated or AI-inferred source material.
21    Ai,
22    /// Code source material.
23    Code,
24    /// External source material outside the local system.
25    External,
26    /// Explicit extension category owned by a downstream crate or product.
27    Custom(String),
28}
29
30impl SourceKind {
31    /// Creates a custom source kind extension.
32    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    /// Returns true when this is a downstream-owned custom extension.
47    pub fn is_custom(&self) -> bool {
48        matches!(self, Self::Custom(_))
49    }
50
51    /// Returns the stable serialized string for validated source kinds.
52    ///
53    /// Use [`Self::try_serialized_value`] when `Custom` may have been constructed directly.
54    pub fn serialized_value(&self) -> String {
55        self.try_serialized_value()
56            .unwrap_or_else(|_| CUSTOM_PREFIX.to_owned())
57    }
58
59    /// Returns the stable serialized string after validating custom extensions.
60    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/// Portable reference to source material.
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct SourceRef {
134    /// Source category.
135    pub kind: SourceKind,
136    /// Optional stable URI for source material.
137    pub uri: Option<String>,
138    /// Optional human-readable source title.
139    pub title: Option<String>,
140    /// Optional stable text capture time, such as RFC 3339.
141    pub captured_at: Option<String>,
142    /// Optional identifier meaningful within the source system.
143    pub source_local_id: Option<String>,
144}
145
146impl SourceRef {
147    /// Creates a source reference with no optional metadata.
148    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    /// Returns this source reference with a validated URI.
159    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    /// Returns this source reference with a validated title.
165    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    /// Returns this source reference with a validated capture timestamp payload.
171    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    /// Returns this source reference with a validated source-local identifier.
177    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    /// Validates custom source kind and optional payload fields.
184    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}