use triblespace::core::metadata;
use triblespace::macros::id_hex;
use triblespace::prelude::*;
pub type TextHandle = Inline<inlineencodings::Handle<blobencodings::UTF8String>>;
pub const DEFAULT_SCOPE_ID: Id = id_hex!("5E2E291C8CD660400D807E916EDEAF1D");
pub const KIND_VERSION_ID: Id = id_hex!("1AA0310347EDFED7874E8BFECC6438CF");
pub const TAG_ARCHIVED_ID: Id = id_hex!("480CB6A663C709478A26A8B49F366C3F");
pub const TAG_SPECS: [(Id, &str); 9] = [
(KIND_VERSION_ID, "version"),
(id_hex!("1A7FB717FBFCA81CA3AA7D3D186ACC8F"), "hypothesis"),
(id_hex!("72CE6B03E39A8AAC37BC0C4015ED54E2"), "critique"),
(id_hex!("243AE22C5E020F61EBBC8C0481BF05A4"), "finding"),
(id_hex!("8871C1709EBFCDD2588369003D3964DE"), "paper"),
(id_hex!("7D58EBA4E1E4A1EF868C3C4A58AEC22E"), "source"),
(id_hex!("C86BCF906D270403A0A2083BB95B3552"), "concept"),
(id_hex!("F8172CC4E495817AB52D2920199EF4BD"), "experiment"),
(TAG_ARCHIVED_ID, "archived"),
];
pub const KIND_REVISION: Id = id_hex!("F2442F00FB816AE01EC450AA5FFE806F");
pub const KIND_AUTHORSHIP: Id = id_hex!("46130A9EC858D471273BAB519C6FE01A");
pub mod attrs {
use super::*;
attributes! {
"FF2DD2D2E71E8CD4AC45C0667FECAF4A" as revision: inlineencodings::GenId;
"838CC157FFDD37C6AC7CC5A472E43ADB" unsafe as author: inlineencodings::GenId;
"EBFC56D50B748E38A14F5FC768F1B9C1" unsafe as fragment: inlineencodings::GenId;
"6DBBE746B7DD7A4793CA098AB882F553" unsafe as content: inlineencodings::Handle<blobencodings::UTF8String>;
"78BABEF1792531A2E51A372D96FE5F3E" unsafe as title: inlineencodings::Handle<blobencodings::UTF8String>;
"DEAFB7E307DF72389AD95A850F24BAA5" unsafe as links_to: inlineencodings::GenId;
"C61CA2F2A70103FD79E97C2F88B854D8" unsafe as references_file_content: inlineencodings::Handle<blobencodings::RawBytes>;
"C98FE0EF9151F196D8F7D816ABBBCC49" unsafe as references_file: inlineencodings::GenId;
}
}
pub fn revision_fragment(
author: Id,
title: &str,
content: &str,
tags: &[Id],
supersedes: &[Id],
) -> Fragment {
let mut fragment = Fragment::empty();
let title: TextHandle = fragment.put(title.to_owned());
let content: TextHandle = fragment.put(content.to_owned());
fragment += revision_fragment_from_handles(author, title, content, tags, supersedes);
fragment
}
pub fn revision_fragment_from_handles(
author: Id,
title: TextHandle,
content: TextHandle,
tags: &[Id],
supersedes: &[Id],
) -> Fragment {
let mut tags: Vec<Id> = tags.to_vec();
tags.push(KIND_REVISION);
tags.sort_unstable();
tags.dedup();
let mut supersedes: Vec<Id> = supersedes.to_vec();
supersedes.sort_unstable();
supersedes.dedup();
entity! {
attrs::author: author,
attrs::title: title,
attrs::content: content,
metadata::tag*: tags.iter(),
metadata::supersedes*: supersedes.iter(),
}
}
pub fn authorship_fragment(
revision: Id,
author: Option<Id>,
authored_at: Option<Inline<inlineencodings::NsTAIInterval>>,
) -> Option<Fragment> {
if author.is_none() && authored_at.is_none() {
return None;
}
Some(entity! {
attrs::revision: revision,
attrs::author?: author,
metadata::created_at?: authored_at,
metadata::tag: KIND_AUTHORSHIP,
})
}
#[cfg(test)]
mod revision_tests {
use super::*;
use hifitime::Epoch;
use triblespace::macros::id_hex;
const TAG_A: Id = id_hex!("D1000000000000000000000000000001");
const TAG_B: Id = id_hex!("D1000000000000000000000000000002");
const AUTHOR: Id = id_hex!("D3000000000000000000000000000001");
const AUTHOR2: Id = id_hex!("D3000000000000000000000000000002");
const AUTHOR_A: Id = id_hex!("A9000000000000000000000000000001");
const AUTHOR_B: Id = id_hex!("A9000000000000000000000000000002");
fn rev(title: &str, content: &str, tags: &[Id], supersedes: &[Id]) -> Id {
revision_fragment(AUTHOR_A, title, content, tags, supersedes)
.root()
.expect("revision is intrinsic, so it always has a root")
}
#[test]
fn identical_state_converges() {
assert_eq!(
rev("T", "body", &[TAG_A], &[]),
rev("T", "body", &[TAG_A], &[])
);
}
#[test]
fn tags_are_a_set() {
assert_eq!(
rev("T", "body", &[TAG_A, TAG_B], &[]),
rev("T", "body", &[TAG_B, TAG_A, TAG_A], &[])
);
}
#[test]
fn retagging_is_a_new_revision() {
let base = rev("T", "body", &[TAG_A], &[]);
let added = rev("T", "body", &[TAG_A, TAG_B], &[]);
let removed = rev("T", "body", &[], &[]);
assert_ne!(base, added);
assert_ne!(base, removed);
assert_ne!(added, removed);
}
#[test]
fn a_revert_does_not_collide_with_what_it_restores() {
let v1 = rev("T", "first", &[], &[]);
let v2 = rev("T", "second", &[], &[v1]);
let reverted = rev("T", "first", &[], &[v2]);
assert_ne!(
reverted, v1,
"same content, different history, different revision"
);
assert_ne!(reverted, v2);
}
#[test]
fn supersedes_is_a_set_and_orders_do_not_matter() {
let a = rev("T", "a", &[], &[]);
let b = rev("T", "b", &[], &[]);
assert_eq!(
rev("T", "merged", &[], &[a, b]),
rev("T", "merged", &[], &[b, a])
);
}
#[test]
fn a_different_author_is_a_different_revision() {
let a = revision_fragment(AUTHOR_A, "T", "body", &[], &[])
.root()
.unwrap();
let b = revision_fragment(AUTHOR_B, "T", "body", &[], &[])
.root()
.unwrap();
assert_ne!(a, b);
}
#[test]
fn the_same_author_repeating_themselves_converges() {
let a = revision_fragment(AUTHOR_A, "T", "body", &[], &[])
.root()
.unwrap();
let b = revision_fragment(AUTHOR_A, "T", "body", &[], &[])
.root()
.unwrap();
assert_eq!(a, b);
}
#[test]
fn authorship_is_separate_from_state() {
let r = rev("T", "body", &[], &[]);
let e = Epoch::from_tai_seconds(1_000.0);
let at = (e, e).try_to_inline().expect("TAI interval");
let a = authorship_fragment(r, Some(AUTHOR), Some(at))
.unwrap()
.root()
.unwrap();
let b = authorship_fragment(r, Some(AUTHOR2), Some(at))
.unwrap()
.root()
.unwrap();
assert_ne!(a, b, "different authors, different records");
assert_eq!(
r,
rev("T", "body", &[], &[]),
"same one revision underneath"
);
}
#[test]
fn empty_provenance_emits_no_record() {
let r = rev("T", "body", &[], &[]);
assert!(authorship_fragment(r, None, None).is_none());
assert!(authorship_fragment(r, Some(AUTHOR), None).is_some());
}
}
pub fn extract_link_targets(content: &str) -> Vec<String> {
let re = regex::Regex::new(r#"#link\("wiki:(?:[a-zA-Z_][a-zA-Z0-9_]*:)?([0-9a-fA-F]{32})"\)"#)
.expect("static regex");
let mut out: Vec<String> = re
.captures_iter(content)
.map(|c| c[1].to_lowercase())
.collect();
out.sort();
out.dedup();
out
}
#[cfg(test)]
mod link_extraction_tests {
use super::*;
#[test]
fn extraction_is_ambient_independent() {
let content = r#"see #link("wiki:0B7BAAC2A1D34E5F8091A2B3C4D5E6F7") and
#link("wiki:cites:AABBCCDDEEFF00112233445566778899")"#;
let got = extract_link_targets(content);
assert_eq!(
got,
vec![
"0b7baac2a1d34e5f8091a2b3c4d5e6f7".to_string(),
"aabbccddeeff00112233445566778899".to_string(),
]
);
}
#[test]
fn unresolvable_targets_are_still_links() {
let got = extract_link_targets(r#"#link("wiki:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")"#);
assert_eq!(got, vec!["ffffffffffffffffffffffffffffffff".to_string()]);
}
#[test]
fn repeated_targets_dedupe_and_non_links_are_ignored() {
let content = r#"#link("wiki:0B7BAAC2A1D34E5F8091A2B3C4D5E6F7")
#link("wiki:0b7baac2a1d34e5f8091a2b3c4d5e6f7")
wiki:0B7BAAC2A1D34E5F8091A2B3C4D5E6F7 bare, and
#link("files:0B7BAAC2A1D34E5F8091A2B3C4D5E6F7")"#;
assert_eq!(
extract_link_targets(content),
vec!["0b7baac2a1d34e5f8091a2b3c4d5e6f7".to_string()]
);
}
}