use crate::computation::{Attester, Executor, Parameter, ATTESTED_COMPUTATION_TYPE};
use crate::date::{Date, DateField, DateTimeField};
use crate::provenance::{Source, UsageWindow};
use crate::trust::{self, Generated, Status, TrustTier, Verification};
use crate::yaml::{Mapping, Value};
pub const REQUIRED_FRONTMATTER_KEYS: [&str; 1] = ["type"];
pub const RECOMMENDED_FRONTMATTER_KEYS: [&str; 3] = ["title", "description", "generated"];
pub const LEGACY_FRONTMATTER_KEYS: [&str; 1] = ["timestamp"];
pub const KNOWN_FRONTMATTER_KEYS: [&str; 17] = [
"type",
"title",
"description",
"resource",
"tags",
"sources",
"usage_window",
"generated",
"verified",
"status",
"stale_after",
"runtime",
"parameters",
"computation",
"executor",
"attester",
"timestamp",
];
pub const PREFERRED_KEY_ORDER: [&str; 11] = [
"type",
"resource",
"title",
"description",
"tags",
"status",
"generated",
"verified",
"stale_after",
"sources",
"usage_window",
];
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Frontmatter {
map: Mapping,
}
impl Frontmatter {
pub fn new() -> Self {
Frontmatter {
map: Mapping::new(),
}
}
pub fn from_mapping(map: Mapping) -> Self {
Frontmatter { map }
}
pub fn as_mapping(&self) -> &Mapping {
&self.map
}
pub fn as_mapping_mut(&mut self) -> &mut Mapping {
&mut self.map
}
pub fn into_mapping(self) -> Mapping {
self.map
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.map.get(key)
}
pub fn set(&mut self, key: impl Into<String>, value: Value) {
self.map.insert(key, value);
}
pub fn reorder_preferred(&mut self) {
let mut ordered = Mapping::new();
for key in PREFERRED_KEY_ORDER {
if let Some(value) = self.map.get(key) {
ordered.insert(key, value.clone());
}
}
for (key, value) in self.map.iter() {
let already_placed = key
.as_str()
.is_some_and(|k| PREFERRED_KEY_ORDER.contains(&k));
if !already_placed {
ordered.push_raw(key.clone(), value.clone());
}
}
self.map = ordered;
}
pub fn type_(&self) -> Option<String> {
self.string("type")
}
pub fn title(&self) -> Option<String> {
self.string("title")
}
pub fn description(&self) -> Option<String> {
self.string("description")
}
pub fn resource(&self) -> Option<String> {
self.string("resource")
}
pub fn tags(&self) -> Vec<String> {
match self.map.get("tags") {
Some(Value::Sequence(items)) => {
items.iter().filter_map(Value::as_display_string).collect()
}
_ => Vec::new(),
}
}
pub fn sources(&self) -> Vec<Source> {
self.map
.get("sources")
.map(Source::list_from_value)
.unwrap_or_default()
}
pub fn usage_window(&self) -> Option<UsageWindow> {
self.map
.get("usage_window")
.and_then(UsageWindow::from_value)
}
pub fn generated(&self) -> Option<Generated> {
self.map.get("generated").and_then(Generated::from_value)
}
pub fn verified(&self) -> Vec<Verification> {
self.map
.get("verified")
.map(Verification::list_from_value)
.unwrap_or_default()
}
pub fn latest_verification(&self) -> Option<Verification> {
let events = self.verified();
trust::latest_verification(&events).cloned()
}
pub fn trust_tier(&self) -> TrustTier {
TrustTier::derive(&self.verified())
}
pub fn content_changed_at(&self) -> Option<DateTimeField> {
self.generated()
.and_then(|g| g.at)
.or_else(|| self.timestamp().map(DateTimeField::new))
}
pub fn timestamp(&self) -> Option<String> {
self.string("timestamp")
}
pub fn status(&self) -> Status {
Status::parse(self.string("status").as_deref())
}
pub fn stale_after(&self) -> Option<DateField> {
self.string("stale_after").map(DateField::new)
}
pub fn is_stale_on(&self, today: Date) -> bool {
let stale_after = self.stale_after().and_then(|d| d.effective_date());
trust::is_stale_on(stale_after, today)
}
pub fn is_attested_computation(&self) -> bool {
self.type_().as_deref() == Some(ATTESTED_COMPUTATION_TYPE)
}
pub fn runtime(&self) -> Option<String> {
self.string("runtime")
}
pub fn parameters(&self) -> Vec<Parameter> {
self.map
.get("parameters")
.map(Parameter::list_from_value)
.unwrap_or_default()
}
pub fn computation(&self) -> Option<String> {
self.string("computation")
}
pub fn executor(&self) -> Option<Executor> {
self.map.get("executor").and_then(Executor::from_value)
}
pub fn attester(&self) -> Option<Attester> {
self.map.get("attester").and_then(Attester::from_value)
}
pub fn path_fields(&self) -> Vec<(&'static str, String)> {
let mut out = Vec::new();
let mut push = |name: &'static str, value: Option<String>| {
if let Some(v) = value.filter(|v| !v.trim().is_empty()) {
out.push((name, v));
}
};
push("resource", self.resource());
push("computation", self.computation());
push(
"executor.resource",
self.executor().and_then(|e| e.resource),
);
push(
"attester.resource",
self.attester().and_then(|a| a.resource),
);
out
}
pub fn extension_keys(&self) -> Vec<&str> {
self.map
.keys()
.filter(|k| !KNOWN_FRONTMATTER_KEYS.contains(k))
.collect()
}
pub fn legacy_keys(&self) -> Vec<&str> {
self.map
.keys()
.filter(|k| LEGACY_FRONTMATTER_KEYS.contains(k))
.collect()
}
fn string(&self, key: &str) -> Option<String> {
self.map.get(key).and_then(Value::as_display_string)
}
}
impl From<Mapping> for Frontmatter {
fn from(map: Mapping) -> Self {
Frontmatter { map }
}
}