use sha2::{Digest, Sha256};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementId {
pub id: String,
pub content_hash: String,
}
impl ElementId {
pub fn new(id: impl Into<String>, content: &str) -> Self {
let id = id.into();
let content_hash = Self::hash_content(content);
Self { id, content_hash }
}
pub fn from_id(id: impl Into<String>) -> Self {
Self {
id: id.into(),
content_hash: String::new(),
}
}
fn hash_content(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("sha256:{:x}", hasher.finalize())
}
pub fn verify(&self, content: &str) -> bool {
if self.content_hash.is_empty() {
return true; }
Self::hash_content(content) == self.content_hash
}
pub fn as_str(&self) -> &str {
&self.id
}
}
impl fmt::Display for ElementId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
impl From<String> for ElementId {
fn from(id: String) -> Self {
Self::from_id(id)
}
}
impl From<&str> for ElementId {
fn from(id: &str) -> Self {
Self::from_id(id)
}
}
pub struct CodeIdGenerator {
namespace: String,
}
impl CodeIdGenerator {
pub fn new(namespace: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
}
}
pub fn module_id(&self, module_name: &str, docs: &str) -> ElementId {
let id = format!("{}.{}", self.namespace, module_name);
ElementId::new(id, docs)
}
pub fn struct_id(&self, module_name: &str, struct_name: &str, docs: &str) -> ElementId {
let id = format!("{}.{}.{}", self.namespace, module_name, struct_name);
ElementId::new(id, docs)
}
pub fn method_id(
&self,
module_name: &str,
struct_name: &str,
method_name: &str,
signature: &str,
docs: &str,
) -> ElementId {
let id = format!(
"{}.{}.{}.{}",
self.namespace, module_name, struct_name, method_name
);
let content = format!("{}\n{}", signature, docs);
ElementId::new(id, &content)
}
pub fn function_id(&self, module_name: &str, function_name: &str, docs: &str) -> ElementId {
let id = format!("{}.{}.{}", self.namespace, module_name, function_name);
ElementId::new(id, docs)
}
pub fn enum_id(&self, module_name: &str, enum_name: &str, docs: &str) -> ElementId {
let id = format!("{}.{}.{}", self.namespace, module_name, enum_name);
ElementId::new(id, docs)
}
pub fn trait_id(&self, module_name: &str, trait_name: &str, docs: &str) -> ElementId {
let id = format!("{}.{}.{}", self.namespace, module_name, trait_name);
ElementId::new(id, docs)
}
}
pub struct LegalIdGenerator {
document_id: String,
}
impl LegalIdGenerator {
pub fn new(document_id: impl Into<String>) -> Self {
Self {
document_id: document_id.into(),
}
}
pub fn article_id(&self, article_num: &str, content: &str) -> ElementId {
let id = format!(
"{}.art.{}",
self.document_id,
Self::normalize_num(article_num)
);
ElementId::new(id, content)
}
pub fn section_id(&self, article_num: &str, section_num: &str, content: &str) -> ElementId {
let id = format!(
"{}.art.{}.sec.{}",
self.document_id,
Self::normalize_num(article_num),
Self::normalize_num(section_num)
);
ElementId::new(id, content)
}
pub fn clause_id(
&self,
article_num: &str,
section_num: &str,
clause_num: &str,
content: &str,
) -> ElementId {
let id = format!(
"{}.art.{}.sec.{}.cl.{}",
self.document_id,
Self::normalize_num(article_num),
Self::normalize_num(section_num),
Self::normalize_num(clause_num)
);
ElementId::new(id, content)
}
pub fn paragraph_id(
&self,
article_num: &str,
section_num: &str,
clause_num: &str,
para_num: &str,
content: &str,
) -> ElementId {
let id = format!(
"{}.art.{}.sec.{}.cl.{}.para.{}",
self.document_id,
Self::normalize_num(article_num),
Self::normalize_num(section_num),
Self::normalize_num(clause_num),
Self::normalize_num(para_num)
);
ElementId::new(id, content)
}
pub fn amendment_id(&self, amendment_num: &str, content: &str) -> ElementId {
let id = format!(
"{}.amendment.{}",
self.document_id,
Self::normalize_num(amendment_num)
);
ElementId::new(id, content)
}
fn normalize_num(num: &str) -> String {
num.trim().to_lowercase().replace(' ', "-")
}
}
pub struct BookstackIdGenerator;
impl BookstackIdGenerator {
pub fn new() -> Self {
Self
}
pub fn book_id(title: &str, description: &str) -> ElementId {
let slug = Self::slugify(title);
let id = format!("book-{}", slug);
ElementId::new(id, &format!("{}\n{}", title, description))
}
pub fn chapter_id(book_slug: &str, chapter_title: &str, content: &str) -> ElementId {
let chapter_slug = Self::slugify(chapter_title);
let id = format!("{}.chapter-{}", book_slug, chapter_slug);
ElementId::new(id, content)
}
pub fn page_id(
book_slug: &str,
chapter_slug: &str,
page_title: &str,
content: &str,
) -> ElementId {
let page_slug = Self::slugify(page_title);
let id = format!("{}.{}.page-{}", book_slug, chapter_slug, page_slug);
ElementId::new(id, content)
}
pub fn shelf_id(name: &str, description: &str) -> ElementId {
let slug = Self::slugify(name);
let id = format!("shelf-{}", slug);
ElementId::new(id, &format!("{}\n{}", name, description))
}
fn slugify(text: &str) -> String {
let slug = text
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() {
c
} else if c.is_whitespace() || c == '-' || c == '_' {
'-'
} else {
' ' }
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join("-");
let mut result = String::new();
let mut last_was_dash = false;
for c in slug.chars() {
if c == '-' {
if !last_was_dash {
result.push(c);
last_was_dash = true;
}
} else {
result.push(c);
last_was_dash = false;
}
}
result.trim_matches('-').to_string()
}
}
impl Default for BookstackIdGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_element_id_creation() {
let id = ElementId::new("std.vec.Vec.push", "Pushes an item onto the vector");
assert_eq!(id.id, "std.vec.Vec.push");
assert!(id.content_hash.starts_with("sha256:"));
assert!(id.verify("Pushes an item onto the vector"));
assert!(!id.verify("Different content"));
}
#[test]
fn test_code_id_generator() {
let gen = CodeIdGenerator::new("std");
let module_id = gen.module_id("vec", "Vector module documentation");
assert_eq!(module_id.id, "std.vec");
let struct_id = gen.struct_id("vec", "Vec", "A contiguous growable array");
assert_eq!(struct_id.id, "std.vec.Vec");
let method_id = gen.method_id(
"vec",
"Vec",
"push",
"pub fn push(&mut self, value: T)",
"Pushes an item",
);
assert_eq!(method_id.id, "std.vec.Vec.push");
}
#[test]
fn test_legal_id_generator() {
let gen = LegalIdGenerator::new("us.constitution");
let article_id = gen.article_id("I", "Article I content");
assert_eq!(article_id.id, "us.constitution.art.i");
let section_id = gen.section_id("I", "8", "Section 8 content");
assert_eq!(section_id.id, "us.constitution.art.i.sec.8");
let clause_id = gen.clause_id("I", "8", "3", "Commerce Clause");
assert_eq!(clause_id.id, "us.constitution.art.i.sec.8.cl.3");
let amendment_id = gen.amendment_id("XIV", "Amendment XIV content");
assert_eq!(amendment_id.id, "us.constitution.amendment.xiv");
}
#[test]
fn test_bookstack_id_generator() {
let book_id =
BookstackIdGenerator::book_id("Rust Programming Guide", "A comprehensive guide");
assert_eq!(book_id.id, "book-rust-programming-guide");
let chapter_id = BookstackIdGenerator::chapter_id(
"book-rust-guide",
"Getting Started",
"Chapter content",
);
assert_eq!(chapter_id.id, "book-rust-guide.chapter-getting-started");
let page_id = BookstackIdGenerator::page_id(
"book-rust-guide",
"chapter-getting-started",
"Installation & Setup",
"Page content",
);
assert_eq!(
page_id.id,
"book-rust-guide.chapter-getting-started.page-installation-setup"
);
}
#[test]
fn test_slugify() {
assert_eq!(BookstackIdGenerator::slugify("Hello World"), "hello-world");
assert_eq!(
BookstackIdGenerator::slugify("C++ Programming"),
"c-programming"
);
assert_eq!(
BookstackIdGenerator::slugify("Multiple Spaces"),
"multiple-spaces"
);
assert_eq!(BookstackIdGenerator::slugify("Trim-Dashes-"), "trim-dashes");
}
#[test]
fn test_content_hash_deterministic() {
let id1 = ElementId::new("test", "Same content");
let id2 = ElementId::new("test", "Same content");
assert_eq!(id1.content_hash, id2.content_hash);
let id3 = ElementId::new("test", "Different content");
assert_ne!(id1.content_hash, id3.content_hash);
}
}