use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FindingKey {
namespace: String,
code: String,
fields: BTreeMap<String, String>,
}
impl FindingKey {
pub fn new<I, K, V>(
namespace: impl Into<String>,
code: impl Into<String>,
fields: I,
) -> Result<Self, String>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let namespace = namespace.into();
if namespace.is_empty() {
return Err("finding key namespace must not be empty".to_string());
}
let code = code.into();
if code.is_empty() {
return Err("finding key code must not be empty".to_string());
}
let mut canonical = BTreeMap::new();
for (name, value) in fields {
let name = name.into();
if name.is_empty() {
return Err("finding key field name must not be empty".to_string());
}
if canonical.insert(name.clone(), value.into()).is_some() {
return Err(format!("finding key field `{name}` is duplicated"));
}
}
Ok(Self {
namespace,
code,
fields: canonical,
})
}
pub fn of<I, K, V>(namespace: impl Into<String>, code: impl Into<String>, fields: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self::new(namespace, code, fields)
.expect("fact schemas use non-empty, unique static field names")
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn code(&self) -> &str {
&self.code
}
pub fn fields(&self) -> impl Iterator<Item = (&str, &str)> {
self.fields
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
}
pub fn to_json(&self) -> Value {
serde_json::json!({
"namespace": self.namespace,
"code": self.code,
"fields": self.fields,
})
}
pub(crate) fn from_json(value: &Value) -> Result<Self, String> {
let string = |name: &str| {
value[name]
.as_str()
.ok_or_else(|| format!("finding key is missing string `{name}`"))
};
let fields = value["fields"]
.as_object()
.ok_or_else(|| "finding key `fields` must be an object".to_string())?
.iter()
.map(|(name, value)| {
value
.as_str()
.map(|value| (name.as_str(), value))
.ok_or_else(|| format!("finding key field `{name}` must be a string"))
})
.collect::<Result<Vec<_>, _>>()?;
Self::new(string("namespace")?, string("code")?, fields)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub(crate) text: String,
pub(crate) key: FindingKey,
}
impl Finding {
pub fn new(text: impl Into<String>, key: FindingKey) -> Self {
Self {
text: text.into(),
key,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn key(&self) -> &FindingKey {
&self.key
}
}