tag2upload_service_manager/
t2umeta_abstract.rs

1
2use crate::prelude::*;
3
4//=============== whole message ===============
5
6pub 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
42//---------- helper functions, with main parsing code ----------
43
44pub 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        // skip the blank line after the title
52        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
97//=============== valuew ===============
98
99pub trait Value: FromStr<Err: Display> {}
100
101impl Value for String {}
102
103//=============== accumulators ===============
104
105pub 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
133//---------- helper functions ----------
134
135fn 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//=============== errors ===============
149
150#[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}