Skip to main content

ferrex_model/
subject_key.rs

1use std::fmt;
2
3/// Errors produced when constructing strongly-typed subject keys.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum SubjectKeyError {
6    Empty,
7}
8
9impl fmt::Display for SubjectKeyError {
10    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11        match self {
12            SubjectKeyError::Empty => write!(f, "subject key cannot be empty"),
13        }
14    }
15}
16
17/// Normalized filesystem path key used for scan orchestration and progress
18/// tracking (e.g. `folder_path_norm`, `path_norm`).
19///
20/// This is intentionally a thin wrapper around `String` so:
21/// - call sites can't accidentally pass an arbitrary string without opting in
22/// - serialization remains compact and ergonomic
23#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "serde", serde(transparent))]
26#[cfg_attr(
27    feature = "rkyv",
28    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
29)]
30#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
31pub struct NormalizedPathKey(String);
32
33impl NormalizedPathKey {
34    pub fn new(value: impl Into<String>) -> Result<Self, SubjectKeyError> {
35        let value = value.into();
36        if value.is_empty() {
37            return Err(SubjectKeyError::Empty);
38        }
39        Ok(Self(value))
40    }
41
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    pub fn into_inner(self) -> String {
47        self.0
48    }
49}
50
51impl fmt::Display for NormalizedPathKey {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(&self.0)
54    }
55}
56
57/// Opaque stable identifier that isn't necessarily a filesystem path.
58#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "serde", serde(transparent))]
61#[cfg_attr(
62    feature = "rkyv",
63    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
64)]
65#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
66pub struct OpaqueSubjectKey(String);
67
68impl OpaqueSubjectKey {
69    pub fn new(value: impl Into<String>) -> Result<Self, SubjectKeyError> {
70        let value = value.into();
71        if value.is_empty() {
72            return Err(SubjectKeyError::Empty);
73        }
74        Ok(Self(value))
75    }
76
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80
81    pub fn into_inner(self) -> String {
82        self.0
83    }
84}
85
86impl fmt::Display for OpaqueSubjectKey {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str(&self.0)
89    }
90}
91
92/// Typed identifier for "what this event/job is about".
93///
94/// This replaces stringly-typed `path_key` usage while keeping the payload
95/// lightweight and serializable across process boundaries.
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98#[cfg_attr(
99    feature = "serde",
100    serde(tag = "type", content = "value", rename_all = "snake_case")
101)]
102#[cfg_attr(
103    feature = "rkyv",
104    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
105)]
106#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, PartialEq, Eq, Hash)))]
107pub enum SubjectKey {
108    /// A normalized filesystem path key.
109    Path(NormalizedPathKey),
110    /// A stable identifier in a different keyspace (e.g. fingerprint hash,
111    /// remote provider path, synthetic image key).
112    Opaque(OpaqueSubjectKey),
113}
114
115impl SubjectKey {
116    pub fn path(value: impl Into<String>) -> Result<Self, SubjectKeyError> {
117        Ok(Self::Path(NormalizedPathKey::new(value)?))
118    }
119
120    pub fn opaque(value: impl Into<String>) -> Result<Self, SubjectKeyError> {
121        Ok(Self::Opaque(OpaqueSubjectKey::new(value)?))
122    }
123
124    pub fn as_str(&self) -> &str {
125        match self {
126            SubjectKey::Path(value) => value.as_str(),
127            SubjectKey::Opaque(value) => value.as_str(),
128        }
129    }
130}
131
132impl fmt::Display for SubjectKey {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            SubjectKey::Path(value) => write!(f, "path:{value}"),
136            SubjectKey::Opaque(value) => write!(f, "opaque:{value}"),
137        }
138    }
139}