Skip to main content

kcode_k1_rust_code_document/
lib.rs

1use kcode_k1_rust_package::{LibraryId, SourceFile, SourcePackage};
2use proc_macro2::Span;
3use std::fmt::{Display, Formatter};
4use std::ops::Range;
5use syn::spanned::Spanned;
6use syn::{ImplItem, Item, TraitItem};
7
8const SUPPORTED_PATHS: [&str; 3] = ["Cargo.toml", "Documentation.md", "src/lib.rs"];
9const MIN_CHUNK_SCALARS: usize = 800;
10const DEFAULT_DOCUMENTATION: &str = "# Consumer contract\n\n`run` performs no operation.\n";
11const DEFAULT_CODE: &str = "pub fn run() {}\n";
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct RustCodeDocument {
15    package: SourcePackage,
16}
17
18impl RustCodeDocument {
19    pub fn new(
20        identity: LibraryId,
21        documentation: impl Into<String>,
22        manifest: impl Into<String>,
23        code: impl Into<String>,
24    ) -> Result<Self, RustCodeDocumentError> {
25        let files = vec![
26            SourceFile::new("Documentation.md", documentation.into().into_bytes()),
27            SourceFile::new("Cargo.toml", manifest.into().into_bytes()),
28            SourceFile::new("src/lib.rs", code.into().into_bytes()),
29        ];
30        let package = SourcePackage::new(identity, files)
31            .map_err(|cause| RustCodeDocumentError(cause.to_string()))?;
32        Self::from_source_package(package)
33    }
34
35    pub fn from_source_package(package: SourcePackage) -> Result<Self, RustCodeDocumentError> {
36        let paths: Vec<&str> = package.files().iter().map(|file| file.path()).collect();
37        if paths != SUPPORTED_PATHS {
38            return fail("source package must contain exactly the three supported paths");
39        }
40        for file in package.files() {
41            if std::str::from_utf8(file.bytes()).is_err() {
42                return fail(format!("{} must be UTF-8", file.path()));
43            }
44        }
45        Ok(Self { package })
46    }
47
48    pub fn approved_default(identity: LibraryId) -> Result<Self, RustCodeDocumentError> {
49        let family = identity.family();
50        let manifest = format!(
51            "[package]\nname = \"{}\"\nversion = \"{}\"\nedition = \"2024\"\nrust-version = \"1.97\"\nlicense = \"MIT\"\n",
52            family.package_name(),
53            identity.version(),
54        );
55        Self::new(identity, DEFAULT_DOCUMENTATION, manifest, DEFAULT_CODE)
56    }
57
58    pub fn identity(&self) -> &LibraryId {
59        self.package.id()
60    }
61
62    pub fn documentation(&self) -> &str {
63        source_text(&self.package, "Documentation.md")
64    }
65
66    pub fn manifest(&self) -> &str {
67        source_text(&self.package, "Cargo.toml")
68    }
69
70    pub fn code(&self) -> &str {
71        source_text(&self.package, "src/lib.rs")
72    }
73
74    pub fn into_source_package(self) -> SourcePackage {
75        self.package
76    }
77
78    pub fn code_chunk_ranges(&self) -> Vec<Range<usize>> {
79        chunk_ranges(self.code())
80    }
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct RustCodeDocumentError(String);
85
86impl RustCodeDocumentError {
87    pub fn message(&self) -> &str {
88        &self.0
89    }
90}
91
92impl Display for RustCodeDocumentError {
93    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
94        formatter.write_str(&self.0)
95    }
96}
97
98impl std::error::Error for RustCodeDocumentError {}
99
100fn fail<T>(message: impl Into<String>) -> Result<T, RustCodeDocumentError> {
101    Err(RustCodeDocumentError(message.into()))
102}
103
104fn source_text<'a>(package: &'a SourcePackage, path: &str) -> &'a str {
105    let file = package
106        .files()
107        .iter()
108        .find(|file| file.path() == path)
109        .expect("validated document path");
110    std::str::from_utf8(file.bytes()).expect("validated UTF-8 document")
111}
112
113fn whole_range(end: usize) -> Vec<Range<usize>> {
114    std::iter::once(0..end).collect()
115}
116
117fn chunk_ranges(source: &str) -> Vec<Range<usize>> {
118    if source.is_empty() {
119        return whole_range(0);
120    }
121    let Some(points) = safe_points(source) else {
122        return whole_range(source.len());
123    };
124    let mut ranges = Vec::new();
125    let mut start = 0;
126    for end in points {
127        if end <= start || end >= source.len() {
128            continue;
129        }
130        if source[start..end].chars().count() < MIN_CHUNK_SCALARS
131            || source[end..].chars().count() < MIN_CHUNK_SCALARS
132        {
133            continue;
134        }
135        ranges.push(start..end);
136        start = end;
137    }
138    ranges.push(start..source.len());
139    ranges
140}
141
142fn safe_points(source: &str) -> Option<Vec<usize>> {
143    let file = syn::parse_file(source).ok()?;
144    let mut points = Vec::new();
145    for item in &file.items {
146        let outer = checked_span(item.span(), source)?;
147        match item {
148            Item::Impl(item) => {
149                for member in &item.items {
150                    match member {
151                        ImplItem::Const(_)
152                        | ImplItem::Fn(_)
153                        | ImplItem::Type(_)
154                        | ImplItem::Macro(_) => {
155                            add_member(member.span(), &outer, source, &mut points)?
156                        }
157                        ImplItem::Verbatim(_) => return None,
158                        _ => return None,
159                    }
160                }
161            }
162            Item::Trait(item) => {
163                for member in &item.items {
164                    match member {
165                        TraitItem::Const(_)
166                        | TraitItem::Fn(_)
167                        | TraitItem::Type(_)
168                        | TraitItem::Macro(_) => {
169                            add_member(member.span(), &outer, source, &mut points)?
170                        }
171                        TraitItem::Verbatim(_) => return None,
172                        _ => return None,
173                    }
174                }
175            }
176            Item::Const(_)
177            | Item::Enum(_)
178            | Item::ExternCrate(_)
179            | Item::Fn(_)
180            | Item::ForeignMod(_)
181            | Item::Macro(_)
182            | Item::Mod(_)
183            | Item::Static(_)
184            | Item::Struct(_)
185            | Item::TraitAlias(_)
186            | Item::Type(_)
187            | Item::Union(_)
188            | Item::Use(_) => {}
189            Item::Verbatim(_) => return None,
190            _ => return None,
191        }
192        points.push(outer.end);
193    }
194    points.sort_unstable();
195    points.dedup();
196    Some(points)
197}
198
199fn checked_span(span: Span, source: &str) -> Option<Range<usize>> {
200    let range = span.byte_range();
201    if range.start >= range.end
202        || range.end > source.len()
203        || !source.is_char_boundary(range.start)
204        || !source.is_char_boundary(range.end)
205        || source[range.clone()].trim().is_empty()
206    {
207        return None;
208    }
209    Some(range)
210}
211
212fn add_member(
213    span: Span,
214    outer: &Range<usize>,
215    source: &str,
216    points: &mut Vec<usize>,
217) -> Option<()> {
218    let member = checked_span(span, source)?;
219    if member.start < outer.start || member.end > outer.end {
220        return None;
221    }
222    points.push(member.end);
223    Some(())
224}