kcode_k1_rust_code_document/
lib.rs1use 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}