#![forbid(unsafe_code)]
#![deny(missing_docs)]
mod contract;
mod document;
mod index;
mod naming;
mod overview;
pub mod pack;
pub mod review;
pub mod route;
pub mod seal;
mod sequence;
mod structure;
pub mod tiers;
use std::collections::BTreeMap;
use sealmap_model::{Codebase, SourcePath, Symbol, SymbolId};
pub use contract::{Drift, DriftEntry, Report, corpus_hash, read_dir_corpus, verify, verify_against, write};
pub use index::{CallRef, DocumentEntry, FragmentEntry, FragmentKind, Index};
pub const CORPUS_SCHEMA_VERSION: u32 = 2;
pub const DOC_SUFFIX: &str = ".md";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExternalLanes {
#[default]
CrateRoot,
Owner,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CorpusOptions {
pub min_calls: usize,
pub max_messages: usize,
pub max_edges: usize,
pub max_entities: usize,
pub include_private: bool,
pub external_lanes: ExternalLanes,
pub emit_model: bool,
pub pretty_json: bool,
}
impl Default for CorpusOptions {
fn default() -> Self {
Self {
min_calls: 1,
max_messages: 80,
max_edges: 300,
max_entities: 60,
include_private: true,
external_lanes: ExternalLanes::CrateRoot,
emit_model: true,
pretty_json: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Corpus {
pub files: BTreeMap<SourcePath, String>,
pub index: Index,
}
impl Corpus {
pub fn document(&self, path: &str) -> Option<&str> {
SourcePath::new(path).ok().and_then(|p| self.files.get(&p)).map(String::as_str)
}
}
pub fn generate(codebase: &Codebase, options: &CorpusOptions) -> Corpus {
let mut files = BTreeMap::new();
naming::assert_unique_idents(codebase);
let mut index = Index::new(codebase);
let lookup = Lookup::new(codebase);
for file in codebase.files.values() {
let (path, text, entry) = document::render(codebase, &lookup, file, options);
index.documents.push(entry);
files.insert(path, text);
}
index.link_expansions();
let reserved = |name: &str| SourcePath::new(name).expect("reserved names are valid paths");
files.insert(reserved("_overview.md"), overview::render(codebase, options));
files.insert(reserved("_README.md"), readme());
if options.emit_model {
files.insert(reserved("_model.json"), to_json(codebase, options.pretty_json));
}
files.insert(reserved("_index.json"), to_json(&index, options.pretty_json));
Corpus { files, index }
}
pub(crate) struct Lookup<'a> {
by_file: BTreeMap<&'a SourcePath, Vec<&'a Symbol>>,
children: BTreeMap<&'a SymbolId, Vec<&'a Symbol>>,
}
impl<'a> Lookup<'a> {
fn new(cb: &'a Codebase) -> Self {
let mut by_file: BTreeMap<&SourcePath, Vec<&Symbol>> = BTreeMap::new();
let mut children: BTreeMap<&SymbolId, Vec<&Symbol>> = BTreeMap::new();
for s in cb.symbols.values() {
by_file.entry(&s.file).or_default().push(s);
if let Some(p) = &s.parent {
children.entry(p).or_default().push(s);
}
}
Self { by_file, children }
}
pub(crate) fn in_file(&self, path: &SourcePath) -> impl Iterator<Item = &'a Symbol> + '_ {
self.by_file.get(path).into_iter().flatten().copied()
}
pub(crate) fn children(&self, id: &SymbolId) -> impl Iterator<Item = &'a Symbol> + '_ {
self.children.get(id).into_iter().flatten().copied()
}
}
pub fn document_path(source: &SourcePath) -> SourcePath {
source.with_suffix(DOC_SUFFIX)
}
pub fn is_reserved(path: &SourcePath) -> bool {
path.file_name().starts_with('_') && !path.as_str().contains('/')
}
fn to_json(value: &impl serde::Serialize, pretty: bool) -> String {
let mut s = if pretty { serde_json::to_string_pretty(value) } else { serde_json::to_string(value) }
.expect("model types always serialise");
s.push('\n');
s
}
fn readme() -> String {
include_str!("corpus_readme.md").to_owned()
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;