tag2upload_service_manager/
t2umeta_abstract.rs1
2use crate::prelude::*;
3
4pub trait FromTagMessage: Sized {
7 fn from_tag_message(s: &str) -> Result<Self, NFR>;
8}
9
10pub trait HasUpdateItem {
11 fn update_item(&mut self, k: &str, v: Option<&str>) -> Result<(), MIE>;
12}
13
14define_derive_deftly! {
15 export FromTagMessage for struct, expect items:
16
17 ${define MOD { $crate::t2umeta_abstract }}
18
19 impl $MOD::FromTagMessage for $ttype {
20 fn from_tag_message(s: &str) -> Result<Self, NFR> {
21 $MOD::from_tag_message_via_update_item(s)
22 }
23 }
24 impl $MOD::HasUpdateItem for $ttype {
25 fn update_item(&mut self, k: &str, v: Option<&str>)
26 -> Result<(), $MOD::MIE> {
27 use $crate::prelude::*;
28 use $MOD::*;
29 $(
30 if display_eq(k, AsKebabCase(stringify!($fname))) {
31 ItemAccumulator::acc_item(&mut self.$fname, v)
32 } else
33 )
34 {
35 hui_unknown_item(k)
36 }
37 }
38 }
39}
40pub use derive_deftly_template_FromTagMessage;
41
42pub fn from_tag_message_via_update_item<T>(s: &str) -> Result<T, NFR>
45where T: Default + HasUpdateItem
46{
47 let mut build = T::default();
48
49 let body = (|| {
50 let (_title, rhs) = s.split_once('\n')?;
51 let body = rhs.strip_prefix('\n')?;
53 Some(body)
54 })()
55 .ok_or_else(|| NFR::TagWithoutMessageBody)?;
56
57 for l in body.split('\n').filter_map(|l| {
58 let l = l.strip_prefix("[dgit ")?;
59 let l = l.trim_end();
60 let l = l.strip_suffix(']')?;
61
62 if l.starts_with('"') {
63 return None;
64 }
65
66 Some(l)
67 }) {
68 for item in l.split_ascii_whitespace() {
69 let (k, v) = item.split_once('=')
70 .map(|(k, v)| (k, Some(v)))
71 .unwrap_or((item, None));
72
73 build.update_item(k, v).map_err(|error| NFR::BadMetadataItem {
74 item: item.to_owned(),
75 error,
76 })?;
77 }
78 }
79
80 Ok(build)
81}
82
83pub fn hui_unknown_item(k: &str) -> Result<(), MIE> {
84 if k.starts_with(|c: char| {
85 c.is_ascii_lowercase() ||
86 c.is_ascii_digit() ||
87 "-+.=".chars().any(|y| c==y)
88 }) {
89 Ok(())
90 } else if k.starts_with(|c:char | c.is_ascii_uppercase()) {
91 Err(MIE::UnknownCriticalItem)
92 } else {
93 Err(MIE::UnknownSyntax)
94 }
95}
96
97pub trait Value: FromStr<Err: Display> {}
100
101impl Value for String {}
102
103pub trait ItemAccumulator {
106 fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE>;
107}
108
109impl ItemAccumulator for Option<()> {
110 fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
111 if let Some(_) = v {
112 return Err(MIE::NoValueAllowed);
113 }
114 acc_option(self, ())
115 }
116}
117
118impl<T: Value> ItemAccumulator for Option<T> {
119 fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
120 acc_option(self, ai_value(v)?)
121 }
122}
123
124impl<T: Value + Hash + Eq + Debug> ItemAccumulator for HashSet<T> {
125 fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
126 if !self.insert(ai_value(v)?) {
127 return Err(MIE::IdenticalValueRepeated);
128 }
129 Ok(())
130 }
131}
132
133fn ai_value<T: Value>(v: Option<&str>) -> Result<T, MIE> {
136 v
137 .ok_or_else(|| MIE::ValueRequired)?
138 .parse().map_err(|e: T::Err| MIE::BadValue(e.to_string()))
139}
140
141fn acc_option<T>(self_: &mut Option<T>, v: T) -> Result<(), MIE> {
142 if let Some(_) = mem::replace(self_, Some(v)) {
143 return Err(MIE::ItemRepeated);
144 }
145 Ok(())
146}
147
148#[derive(Error, Debug)]
151pub enum MetadataItemError {
152 #[error("unsupported/incorrect syntax")]
153 UnknownSyntax,
154
155 #[error("unknown critical item")]
156 UnknownCriticalItem,
157
158 #[error("no value allowed")]
159 NoValueAllowed,
160
161 #[error("item may only appear once")]
162 ItemRepeated,
163
164 #[error("identical item value repeated")]
165 IdenticalValueRepeated,
166
167 #[error("item requires a value")]
168 ValueRequired,
169
170 #[error("invalid value: {0}")]
171 BadValue(String),
172}
173
174pub use MetadataItemError as MIE;
175
176#[cfg(test)]
177mod test {
178 use super::*;
179 use test_prelude::*;
180
181 #[test]
182 fn debug_tag_meta() -> TestResult<()> {
183
184 #[derive(Debug, Default)]
185 struct DebugTagMeta {
186 settings: BTreeSet<String>,
187 }
188
189 impl HasUpdateItem for DebugTagMeta {
190 fn update_item(&mut self, k: &str, v: Option<&str>)
191 -> Result<(), MIE>
192 {
193 let s = if let Some(v) = v {
194 format!("{k}={v}")
195 } else {
196 format!("{k}")
197 };
198 self.settings.insert(s).then(|| ())
199 .ok_or(MIE::IdenticalValueRepeated)
200 }
201 }
202
203 let tmeta: DebugTagMeta = from_tag_message_via_update_item(
204 serde_json::from_str::<serde_json::Value>(crate::test::HOOK_BODY)?
205 ["message"]
206 .as_str().ok_or_else(|| anyhow!("not a string"))?
207 )?;
208
209 println!("{tmeta:#?}");
210
211 Ok(())
212 }
213}