use serde_json::Value;
use std::collections::BTreeMap;
fn canonical_fields<I, K, V>(kind: &str, fields: I) -> Result<BTreeMap<String, String>, String>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let mut canonical = BTreeMap::new();
for (name, value) in fields {
let name = name.into();
if name.is_empty() {
return Err(format!("{kind} field name must not be empty"));
}
if canonical.insert(name.clone(), value.into()).is_some() {
return Err(format!("{kind} field `{name}` is duplicated"));
}
}
Ok(canonical)
}
fn required_string<'a>(value: &'a Value, kind: &str, name: &str) -> Result<&'a str, String> {
value[name]
.as_str()
.ok_or_else(|| format!("{kind} is missing string `{name}`"))
}
fn json_fields<'a>(value: &'a Value, kind: &str) -> Result<Vec<(&'a str, &'a str)>, String> {
value["fields"]
.as_object()
.ok_or_else(|| format!("{kind} `fields` must be an object"))?
.iter()
.map(|(name, value)| {
value
.as_str()
.map(|value| (name.as_str(), value))
.ok_or_else(|| format!("{kind} field `{name}` must be a string"))
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RuleKey {
rule_type: String,
fields: BTreeMap<String, String>,
}
impl RuleKey {
pub fn new<I, K, V>(rule_type: impl Into<String>, fields: I) -> Result<Self, String>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let rule_type = rule_type.into();
if rule_type.is_empty() {
return Err("rule key type must not be empty".to_string());
}
Ok(Self {
rule_type,
fields: canonical_fields("rule key", fields)?,
})
}
pub fn of<I, K, V>(rule_type: impl Into<String>, fields: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self::new(rule_type, fields)
.expect("rule schemas use non-empty semantic types and unique static field names")
}
pub fn rule_type(&self) -> &str {
&self.rule_type
}
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!({
"type": self.rule_type,
"fields": self.fields,
})
}
pub(crate) fn from_json(value: &Value) -> Result<Self, String> {
Self::new(
required_string(value, "rule key", "type")?,
json_fields(value, "rule key")?,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct StructuredFactIdentity {
fact_type: String,
shape: String,
fields: BTreeMap<String, String>,
}
impl StructuredFactIdentity {
pub fn new<I, K, V>(
fact_type: impl Into<String>,
shape: impl Into<String>,
fields: I,
) -> Result<Self, String>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let fact_type = fact_type.into();
if fact_type.is_empty() {
return Err("fact identity type must not be empty".to_string());
}
let shape = shape.into();
if shape.is_empty() {
return Err("fact identity shape must not be empty".to_string());
}
Ok(Self {
fact_type,
shape,
fields: canonical_fields("fact identity", fields)?,
})
}
pub fn of<I, K, V>(fact_type: impl Into<String>, shape: impl Into<String>, fields: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self::new(fact_type, shape, fields).expect(
"fact schemas use non-empty semantic types/shapes and unique static field names",
)
}
pub fn fact_type(&self) -> &str {
&self.fact_type
}
pub fn shape(&self) -> &str {
&self.shape
}
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!({
"type": self.fact_type,
"shape": self.shape,
"fields": self.fields,
})
}
pub(crate) fn from_semantic_json(value: &Value) -> Result<Self, String> {
Self::new(
required_string(value, "fact identity", "type")?,
required_string(value, "fact identity", "shape")?,
json_fields(value, "fact identity")?,
)
}
}