use kcode_k1_rust_package::{LibraryId, SourceFile, SourcePackage};
use proc_macro2::Span;
use std::fmt::{Display, Formatter};
use std::ops::Range;
use syn::spanned::Spanned;
use syn::{ImplItem, Item, TraitItem};
const SUPPORTED_PATHS: [&str; 3] = ["Cargo.toml", "Documentation.md", "src/lib.rs"];
const MIN_CHUNK_SCALARS: usize = 800;
const DEFAULT_DOCUMENTATION: &str = "# Consumer contract\n\n`run` performs no operation.\n";
const DEFAULT_CODE: &str = "pub fn run() {}\n";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustCodeDocument {
package: SourcePackage,
}
impl RustCodeDocument {
pub fn new(
identity: LibraryId,
documentation: impl Into<String>,
manifest: impl Into<String>,
code: impl Into<String>,
) -> Result<Self, RustCodeDocumentError> {
let files = vec![
SourceFile::new("Documentation.md", documentation.into().into_bytes()),
SourceFile::new("Cargo.toml", manifest.into().into_bytes()),
SourceFile::new("src/lib.rs", code.into().into_bytes()),
];
let package = SourcePackage::new(identity, files)
.map_err(|cause| RustCodeDocumentError(cause.to_string()))?;
Self::from_source_package(package)
}
pub fn from_source_package(package: SourcePackage) -> Result<Self, RustCodeDocumentError> {
let paths: Vec<&str> = package.files().iter().map(|file| file.path()).collect();
if paths != SUPPORTED_PATHS {
return fail("source package must contain exactly the three supported paths");
}
for file in package.files() {
if std::str::from_utf8(file.bytes()).is_err() {
return fail(format!("{} must be UTF-8", file.path()));
}
}
Ok(Self { package })
}
pub fn approved_default(identity: LibraryId) -> Result<Self, RustCodeDocumentError> {
let family = identity.family();
let manifest = format!(
"[package]\nname = \"{}\"\nversion = \"{}\"\nedition = \"2024\"\nrust-version = \"1.97\"\nlicense = \"MIT\"\n",
family.package_name(),
identity.version(),
);
Self::new(identity, DEFAULT_DOCUMENTATION, manifest, DEFAULT_CODE)
}
pub fn identity(&self) -> &LibraryId {
self.package.id()
}
pub fn documentation(&self) -> &str {
source_text(&self.package, "Documentation.md")
}
pub fn manifest(&self) -> &str {
source_text(&self.package, "Cargo.toml")
}
pub fn code(&self) -> &str {
source_text(&self.package, "src/lib.rs")
}
pub fn into_source_package(self) -> SourcePackage {
self.package
}
pub fn code_chunk_ranges(&self) -> Vec<Range<usize>> {
chunk_ranges(self.code())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RustCodeDocumentError(String);
impl RustCodeDocumentError {
pub fn message(&self) -> &str {
&self.0
}
}
impl Display for RustCodeDocumentError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for RustCodeDocumentError {}
fn fail<T>(message: impl Into<String>) -> Result<T, RustCodeDocumentError> {
Err(RustCodeDocumentError(message.into()))
}
fn source_text<'a>(package: &'a SourcePackage, path: &str) -> &'a str {
let file = package
.files()
.iter()
.find(|file| file.path() == path)
.expect("validated document path");
std::str::from_utf8(file.bytes()).expect("validated UTF-8 document")
}
fn whole_range(end: usize) -> Vec<Range<usize>> {
std::iter::once(0..end).collect()
}
fn chunk_ranges(source: &str) -> Vec<Range<usize>> {
if source.is_empty() {
return whole_range(0);
}
let Some(points) = safe_points(source) else {
return whole_range(source.len());
};
let mut ranges = Vec::new();
let mut start = 0;
for end in points {
if end <= start || end >= source.len() {
continue;
}
if source[start..end].chars().count() < MIN_CHUNK_SCALARS
|| source[end..].chars().count() < MIN_CHUNK_SCALARS
{
continue;
}
ranges.push(start..end);
start = end;
}
ranges.push(start..source.len());
ranges
}
fn safe_points(source: &str) -> Option<Vec<usize>> {
let file = syn::parse_file(source).ok()?;
let mut points = Vec::new();
for item in &file.items {
let outer = checked_span(item.span(), source)?;
match item {
Item::Impl(item) => {
for member in &item.items {
match member {
ImplItem::Const(_)
| ImplItem::Fn(_)
| ImplItem::Type(_)
| ImplItem::Macro(_) => {
add_member(member.span(), &outer, source, &mut points)?
}
ImplItem::Verbatim(_) => return None,
_ => return None,
}
}
}
Item::Trait(item) => {
for member in &item.items {
match member {
TraitItem::Const(_)
| TraitItem::Fn(_)
| TraitItem::Type(_)
| TraitItem::Macro(_) => {
add_member(member.span(), &outer, source, &mut points)?
}
TraitItem::Verbatim(_) => return None,
_ => return None,
}
}
}
Item::Const(_)
| Item::Enum(_)
| Item::ExternCrate(_)
| Item::Fn(_)
| Item::ForeignMod(_)
| Item::Macro(_)
| Item::Mod(_)
| Item::Static(_)
| Item::Struct(_)
| Item::TraitAlias(_)
| Item::Type(_)
| Item::Union(_)
| Item::Use(_) => {}
Item::Verbatim(_) => return None,
_ => return None,
}
points.push(outer.end);
}
points.sort_unstable();
points.dedup();
Some(points)
}
fn checked_span(span: Span, source: &str) -> Option<Range<usize>> {
let range = span.byte_range();
if range.start >= range.end
|| range.end > source.len()
|| !source.is_char_boundary(range.start)
|| !source.is_char_boundary(range.end)
|| source[range.clone()].trim().is_empty()
{
return None;
}
Some(range)
}
fn add_member(
span: Span,
outer: &Range<usize>,
source: &str,
points: &mut Vec<usize>,
) -> Option<()> {
let member = checked_span(span, source)?;
if member.start < outer.start || member.end > outer.end {
return None;
}
points.push(member.end);
Some(())
}