use std::collections::BTreeMap;
use fig::Value;
use flower_core::Seg;
use prov::{Cardinality, FieldSpec, OpenClosed, Relation, RelationSet, WorkspaceConfig};
pub const POLICY_KEY: &str = "prov";
pub const IDENTITY_KEY: &str = "id";
pub const TITLE_KEY: &str = "title";
pub const CONTENT_KEY: &str = "content";
pub const MANIFEST_KEY: &str = "manifest";
pub const ATTACHMENT_KEY: &str = "attachment";
pub const CONTENT_HASH_KEY: &str = "content_hash";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Payload {
Content,
Manifest,
Marker,
Digest,
}
#[derive(Debug, Clone)]
pub struct RelationFacet {
pub name: String,
pub cardinality: Cardinality,
pub inverse: Option<String>,
pub spanning: bool,
pub pointer: bool,
pub means: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FieldFacet {
pub name: String,
pub vocabulary: Option<String>,
pub values: OpenClosed,
pub reify: bool,
}
#[derive(Debug, Clone)]
pub enum Facet {
Relation(RelationFacet),
Policy,
Identity,
Title,
Payload(Payload),
Stamp,
Field(FieldFacet),
Carried,
}
impl Facet {
pub fn read_by_prov(&self) -> bool {
!matches!(self, Facet::Carried)
}
pub fn structural(&self) -> bool {
!matches!(self, Facet::Title | Facet::Field(_) | Facet::Carried)
}
pub fn managed(&self) -> bool {
matches!(
self,
Facet::Identity | Facet::Stamp | Facet::Payload(Payload::Digest)
)
}
pub fn relation(&self) -> Option<&RelationFacet> {
match self {
Facet::Relation(rel) => Some(rel),
_ => None,
}
}
pub fn kind(&self) -> &'static str {
match self {
Facet::Relation(rel) if rel.pointer => "pointer",
Facet::Relation(_) => "relation",
Facet::Policy => "policy",
Facet::Identity => "identity",
Facet::Title => "title",
Facet::Payload(_) => "payload",
Facet::Stamp => "stamp",
Facet::Field(_) => "field",
Facet::Carried => "carried",
}
}
}
#[derive(Debug, Clone)]
pub struct Facets {
relations: RelationSet,
by_relation: BTreeMap<String, RelationFacet>,
fields: BTreeMap<String, FieldFacet>,
stamp: Option<String>,
}
impl Default for Facets {
fn default() -> Self {
Self::from_config(&WorkspaceConfig::default())
}
}
impl Facets {
pub fn from_config(config: &WorkspaceConfig) -> Self {
let relations = config.relation_set();
let mut by_relation = BTreeMap::new();
for relation in relations.relations() {
by_relation.insert(
relation.name.clone(),
relation_facet(relation, &relations, config),
);
}
let fields = config
.fields
.iter()
.map(|(name, spec)| (name.clone(), field_facet(name, spec)))
.collect();
Self {
relations,
by_relation,
fields,
stamp: (!config.updated.is_empty()).then(|| config.updated.clone()),
}
}
pub fn relations(&self) -> &RelationSet {
&self.relations
}
pub fn of_key(&self, key: &str) -> Facet {
if let Some(relation) = self.by_relation.get(key) {
return Facet::Relation(relation.clone());
}
if self.stamp.as_deref() == Some(key) {
return Facet::Stamp;
}
match key {
POLICY_KEY => return Facet::Policy,
IDENTITY_KEY => return Facet::Identity,
TITLE_KEY => return Facet::Title,
CONTENT_KEY => return Facet::Payload(Payload::Content),
MANIFEST_KEY => return Facet::Payload(Payload::Manifest),
ATTACHMENT_KEY => return Facet::Payload(Payload::Marker),
CONTENT_HASH_KEY => return Facet::Payload(Payload::Digest),
_ => {}
}
match self.fields.get(key) {
Some(field) => Facet::Field(field.clone()),
None => Facet::Carried,
}
}
pub fn of(&self, path: &[Seg]) -> Facet {
match path.first() {
Some(Seg::Key(key)) => self.of_key(key),
_ => Facet::Carried,
}
}
pub fn classify(&self, meta: &Value) -> Vec<(String, Facet)> {
top_level_keys(meta)
.into_iter()
.map(|key| {
let facet = self.of_key(&key);
(key, facet)
})
.collect()
}
pub fn structural_keys(&self, meta: &Value) -> Vec<String> {
self.keys_where(meta, |facet| facet.structural())
}
pub fn managed_keys(&self, meta: &Value) -> Vec<String> {
self.keys_where(meta, |facet| facet.managed())
}
pub fn managed_key_names(&self) -> Vec<String> {
let mut names = vec![IDENTITY_KEY.to_string(), CONTENT_HASH_KEY.to_string()];
names.extend(self.stamp.clone());
names
}
pub fn carried_keys(&self, meta: &Value) -> Vec<String> {
self.keys_where(meta, |facet| !facet.read_by_prov())
}
fn keys_where(&self, meta: &Value, want: impl Fn(&Facet) -> bool) -> Vec<String> {
self.classify(meta)
.into_iter()
.filter(|(_, facet)| want(facet))
.map(|(key, _)| key)
.collect()
}
}
fn relation_facet(
relation: &Relation,
relations: &RelationSet,
config: &WorkspaceConfig,
) -> RelationFacet {
let name = relation.name.as_str();
let pointer = [
relations.registry_relation(),
relations.config_relation(),
relations.recycle_relation(),
relations.history_relation(),
relations.about_relation(),
]
.into_iter()
.flatten()
.any(|p| p == name);
RelationFacet {
name: relation.name.clone(),
cardinality: relation.cardinality,
inverse: relation.inverse.clone(),
spanning: relations.spanning_relation() == Some(name),
pointer,
means: config
.relation_defs
.get(name)
.and_then(|def| def.means.clone())
.or_else(|| RelationSet::diaryx_means(name).map(str::to_string)),
}
}
fn field_facet(name: &str, spec: &FieldSpec) -> FieldFacet {
FieldFacet {
name: name.to_string(),
vocabulary: spec.vocabulary.clone(),
values: spec.values,
reify: spec.reify,
}
}
fn top_level_keys(meta: &Value) -> Vec<String> {
let Some(entries) = meta.as_mapping() else {
return Vec::new();
};
entries
.iter()
.filter_map(|(key, _)| key.as_str().map(str::to_string))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use prov::{Document, FieldType, RelationDef};
const DOC: &str = "\
---
title: A Note
id: ajp7eq
contents:
- '[Child](child.md)'
part_of: '[Root](/README.md)'
audience: public
mood: rainy
content_hash: sha256-abc
---
# Note
";
fn meta_of(text: &str) -> Value {
let doc = Document::parse("note.md", text).expect("parse");
Value::from(&doc.meta)
}
fn workspace() -> WorkspaceConfig {
let mut config = WorkspaceConfig::default();
config.fields.insert(
"audience".to_string(),
FieldSpec {
ty: None,
values: OpenClosed::Closed,
vocabulary: Some("audiences.yaml".to_string()),
reify: false,
},
);
config.updated = "updated".to_string();
config
}
#[test]
fn separates_provs_own_keys_from_the_ones_it_only_carries() {
let facets = Facets::from_config(&workspace());
let meta = meta_of(DOC);
assert_eq!(
facets.structural_keys(&meta),
["id", "contents", "part_of", "content_hash"],
"prov's structure, in document order"
);
assert_eq!(
facets.carried_keys(&meta),
["mood"],
"only what prov never reads"
);
assert!(facets.of_key("title").read_by_prov());
assert!(!facets.of_key("title").structural());
assert!(facets.of_key("audience").read_by_prov());
assert!(!facets.of_key("audience").structural());
}
#[test]
fn the_workspace_maintains_id_the_digest_and_the_stamp() {
let facets = Facets::from_config(&workspace());
let meta = meta_of(DOC);
assert_eq!(facets.managed_keys(&meta), ["id", "content_hash"]);
assert_eq!(
facets.managed_key_names(),
["id", "content_hash", "updated"]
);
assert_eq!(
Facets::default().managed_key_names(),
["id", "content_hash"],
"no declared stamp, no stamped key"
);
assert!(facets.of_key("updated").managed());
assert!(!Facets::default().of_key("updated").managed());
}
#[test]
fn the_vocabulary_is_the_workspaces_not_this_crates() {
let mut config = WorkspaceConfig::default();
config.relation_defs.insert(
"link_of".to_string(),
RelationDef {
off: true,
..RelationDef::default()
},
);
config.relation_defs.insert(
"see_also".to_string(),
RelationDef {
cardinality: Some(Cardinality::Many),
means: Some("worth reading beside this".to_string()),
..RelationDef::default()
},
);
let facets = Facets::from_config(&config);
assert!(
matches!(facets.of_key("link_of"), Facet::Carried),
"a retracted name is an ordinary field"
);
let see_also = facets
.of_key("see_also")
.relation()
.cloned()
.expect("a declared relation");
assert_eq!(see_also.means.as_deref(), Some("worth reading beside this"));
assert!(!see_also.spanning);
let contents = facets.of_key("contents");
let contents = contents.relation().expect("contents is a relation");
assert!(contents.spanning, "contents is the backbone");
assert_eq!(
contents.means.as_deref(),
Some("documents contained by this one")
);
}
#[test]
fn a_pointer_relation_is_marked_as_machinery() {
let facets = Facets::default();
let config = facets.of_key("config");
let config = config.relation().expect("config is a relation");
assert!(config.pointer, "config points at machinery");
assert!(!config.spanning);
assert_eq!(Facet::Relation(config.clone()).kind(), "pointer");
let contents = facets.of_key("contents");
assert!(!contents.relation().expect("relation").pointer);
}
#[test]
fn a_nested_path_takes_its_top_level_keys_facet() {
let facets = Facets::default();
let nested = [Seg::Key("contents".into()), Seg::Index(2)];
assert!(matches!(facets.of(&nested), Facet::Relation(_)));
assert!(matches!(
facets.of(&[Seg::Key("prov".into()), Seg::Key("spanning".into())]),
Facet::Policy
));
assert!(matches!(facets.of(&[]), Facet::Carried), "the document");
}
#[test]
fn a_declared_field_carries_its_vocabulary() {
let mut config = workspace();
config.fields.insert(
"created".to_string(),
FieldSpec {
ty: Some(FieldType::Str),
values: OpenClosed::default(),
vocabulary: None,
reify: false,
},
);
let facets = Facets::from_config(&config);
match facets.of_key("audience") {
Facet::Field(field) => {
assert_eq!(field.vocabulary.as_deref(), Some("audiences.yaml"));
assert!(matches!(field.values, OpenClosed::Closed));
}
other => panic!("expected a declared field, got {other:?}"),
}
match facets.of_key("created") {
Facet::Field(field) => assert!(field.vocabulary.is_none()),
other => panic!("expected a declared field, got {other:?}"),
}
}
}