1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize)]
7pub struct LabelerConfig {
8 pub did: String,
10 pub signing_key_path: Option<String>,
12 pub signing_key_base64: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Label {
19 #[serde(default = "default_version")]
21 pub ver: i32,
22 pub src: String,
24 pub uri: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub cid: Option<String>,
29 pub val: String,
31 #[serde(default)]
33 pub neg: bool,
34 pub cts: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub exp: Option<String>,
39}
40
41fn default_version() -> i32 {
42 1
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SignedLabel {
48 #[serde(flatten)]
50 pub label: Label,
51 pub sig: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum LabelValue {
60 Porn,
62 Sexual,
64 Nudity,
66 GraphicMedia,
68 Spam,
70 Impersonation,
72 Custom(String),
74}
75
76impl LabelValue {
77 pub fn as_str(&self) -> &str {
79 match self {
80 Self::Porn => "porn",
81 Self::Sexual => "sexual",
82 Self::Nudity => "nudity",
83 Self::GraphicMedia => "graphic-media",
84 Self::Spam => "spam",
85 Self::Impersonation => "impersonation",
86 Self::Custom(s) => s.as_str(),
87 }
88 }
89}
90
91impl std::fmt::Display for LabelValue {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.write_str(self.as_str())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn label_value_as_str() {
103 assert_eq!(LabelValue::Porn.as_str(), "porn");
104 assert_eq!(LabelValue::Sexual.as_str(), "sexual");
105 assert_eq!(LabelValue::Nudity.as_str(), "nudity");
106 assert_eq!(LabelValue::GraphicMedia.as_str(), "graphic-media");
107 assert_eq!(LabelValue::Spam.as_str(), "spam");
108 assert_eq!(LabelValue::Impersonation.as_str(), "impersonation");
109 assert_eq!(
110 LabelValue::Custom("custom-val".into()).as_str(),
111 "custom-val"
112 );
113 }
114
115 #[test]
116 fn label_value_display() {
117 assert_eq!(format!("{}", LabelValue::Porn), "porn");
118 assert_eq!(format!("{}", LabelValue::Custom("test".into())), "test");
119 }
120
121 #[test]
122 fn label_serde_roundtrip() {
123 let label = Label {
124 ver: 1,
125 src: "did:plc:labeler".to_string(),
126 uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
127 cid: Some("bafyreib".to_string()),
128 val: "spam".to_string(),
129 neg: false,
130 cts: "2024-01-01T00:00:00Z".to_string(),
131 exp: None,
132 };
133
134 let json = serde_json::to_string(&label).unwrap();
135 let parsed: Label = serde_json::from_str(&json).unwrap();
136
137 assert_eq!(parsed.ver, 1);
138 assert_eq!(parsed.src, "did:plc:labeler");
139 assert_eq!(parsed.uri, "at://did:plc:user/app.bsky.feed.post/abc");
140 assert_eq!(parsed.cid.as_deref(), Some("bafyreib"));
141 assert_eq!(parsed.val, "spam");
142 assert!(!parsed.neg);
143 assert_eq!(parsed.cts, "2024-01-01T00:00:00Z");
144 assert!(parsed.exp.is_none());
145 }
146
147 #[test]
148 fn label_serde_optional_fields_omitted() {
149 let label = Label {
150 ver: 1,
151 src: "did:plc:labeler".to_string(),
152 uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
153 cid: None,
154 val: "spam".to_string(),
155 neg: false,
156 cts: "2024-01-01T00:00:00Z".to_string(),
157 exp: None,
158 };
159
160 let json = serde_json::to_string(&label).unwrap();
161 assert!(!json.contains("cid"));
162 assert!(!json.contains("exp"));
163 }
164
165 #[test]
166 fn signed_label_flattens() {
167 let signed = SignedLabel {
168 label: Label {
169 ver: 1,
170 src: "did:plc:labeler".to_string(),
171 uri: "at://did:plc:user/post/1".to_string(),
172 cid: None,
173 val: "porn".to_string(),
174 neg: false,
175 cts: "2024-01-01T00:00:00Z".to_string(),
176 exp: None,
177 },
178 sig: "c2lnbmF0dXJl".to_string(),
179 };
180
181 let json = serde_json::to_string(&signed).unwrap();
182 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
183 assert_eq!(v["src"], "did:plc:labeler");
184 assert_eq!(v["sig"], "c2lnbmF0dXJl");
185 }
186}