tag2upload_service_manager/
t2umeta_abstract.rs
use crate::prelude::*;
pub trait FromTagMessage: Sized {
fn from_tag_message(s: &str) -> Result<Self, NFR>;
}
pub trait HasUpdateItem {
fn update_item(&mut self, k: &str, v: Option<&str>) -> Result<(), MIE>;
}
define_derive_deftly! {
export FromTagMessage for struct, expect items:
${define MOD { $crate::t2umeta_abstract }}
impl $MOD::FromTagMessage for $ttype {
fn from_tag_message(s: &str) -> Result<Self, NFR> {
$MOD::from_tag_message_via_update_item(s)
}
}
impl $MOD::HasUpdateItem for $ttype {
fn update_item(&mut self, k: &str, v: Option<&str>)
-> Result<(), $MOD::MIE> {
use $crate::prelude::*;
use $MOD::*;
$(
if display_eq(k, AsKebabCase(stringify!($fname))) {
ItemAccumulator::acc_item(&mut self.$fname, v)
} else
)
{
hui_unknown_item(k)
}
}
}
}
pub use derive_deftly_template_FromTagMessage;
pub fn from_tag_message_via_update_item<T>(s: &str) -> Result<T, NFR>
where T: Default + HasUpdateItem
{
let mut build = T::default();
let body = (|| {
let (_title, rhs) = s.split_once('\n')?;
let body = rhs.strip_prefix('\n')?;
Some(body)
})()
.ok_or_else(|| NFR::TagWithoutMessageBody)?;
for l in body.split('\n').filter_map(|l| {
let l = l.strip_prefix("[dgit ")?;
let l = l.trim_end();
let l = l.strip_suffix(']')?;
if l.starts_with('"') {
return None;
}
Some(l)
}) {
for item in l.split_ascii_whitespace() {
let (k, v) = item.split_once('=')
.map(|(k, v)| (k, Some(v)))
.unwrap_or((item, None));
build.update_item(k, v).map_err(|error| NFR::BadMetadataItem {
item: item.to_owned(),
error,
})?;
}
}
Ok(build)
}
pub fn hui_unknown_item(k: &str) -> Result<(), MIE> {
if k.starts_with(|c: char| {
c.is_ascii_lowercase() ||
c.is_ascii_digit() ||
"-+.=".chars().any(|y| c==y)
}) {
Ok(())
} else if k.starts_with(|c:char | c.is_ascii_uppercase()) {
Err(MIE::UnknownCriticalItem)
} else {
Err(MIE::UnknownSyntax)
}
}
pub trait Value: FromStr<Err: Display> {}
impl Value for String {}
pub trait ItemAccumulator {
fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE>;
}
impl ItemAccumulator for Option<()> {
fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
if let Some(_) = v {
return Err(MIE::NoValueAllowed);
}
acc_option(self, ())
}
}
impl<T: Value> ItemAccumulator for Option<T> {
fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
acc_option(self, ai_value(v)?)
}
}
impl<T: Value + Hash + Eq + Debug> ItemAccumulator for HashSet<T> {
fn acc_item(&mut self, v: Option<&str>) -> Result<(), MIE> {
if !self.insert(ai_value(v)?) {
return Err(MIE::IdenticalValueRepeated);
}
Ok(())
}
}
fn ai_value<T: Value>(v: Option<&str>) -> Result<T, MIE> {
v
.ok_or_else(|| MIE::ValueRequired)?
.parse().map_err(|e: T::Err| MIE::BadValue(e.to_string()))
}
fn acc_option<T>(self_: &mut Option<T>, v: T) -> Result<(), MIE> {
if let Some(_) = mem::replace(self_, Some(v)) {
return Err(MIE::ItemRepeated);
}
Ok(())
}
#[derive(Error, Debug)]
pub enum MetadataItemError {
#[error("unsupported/incorrect syntax")]
UnknownSyntax,
#[error("unknown critical item")]
UnknownCriticalItem,
#[error("no value allowed")]
NoValueAllowed,
#[error("item may only appear once")]
ItemRepeated,
#[error("identical item value repeated")]
IdenticalValueRepeated,
#[error("item requires a value")]
ValueRequired,
#[error("invalid value: {0}")]
BadValue(String),
}
pub use MetadataItemError as MIE;
#[cfg(test)]
mod test {
use super::*;
use test_prelude::*;
#[test]
fn debug_tag_meta() -> TestResult<()> {
#[derive(Debug, Default)]
struct DebugTagMeta {
settings: BTreeSet<String>,
}
impl HasUpdateItem for DebugTagMeta {
fn update_item(&mut self, k: &str, v: Option<&str>)
-> Result<(), MIE>
{
let s = if let Some(v) = v {
format!("{k}={v}")
} else {
format!("{k}")
};
self.settings.insert(s).then(|| ())
.ok_or(MIE::IdenticalValueRepeated)
}
}
let tmeta: DebugTagMeta = from_tag_message_via_update_item(
serde_json::from_str::<serde_json::Value>(crate::test::HOOK_BODY)?
["message"]
.as_str().ok_or_else(|| anyhow!("not a string"))?
)?;
println!("{tmeta:#?}");
Ok(())
}
}