rust_assistant/lib.rs
1//! # Rust Assistant Library
2//!
3//! `rust_assistant` is a comprehensive library designed to enhance the Rust development experience,
4//! offering a suite of tools and functionalities for downloading, caching, searching, and analyzing Rust crates.
5//!
6//! This library encapsulates a range of modules, each specializing in different aspects of crate management
7//! and code analysis. It aims to streamline the process of working with Rust crates, providing developers
8//! with efficient access to crate data, advanced search capabilities, and more.
9//!
10//! ## Features
11//!
12//! - **Crate Downloading**: Facilitates the downloading of crates from sources like crates.io,
13//! handling network requests and data processing.
14//!
15//! - **Crate Caching**: Implements caching mechanisms to store downloaded crates, optimizing
16//! performance and reducing redundant operations.
17//!
18//! - **Search Functionality**: Provides advanced search functionalities within crate contents,
19//! including source code, documentation, and other relevant data.
20//!
21//! ## Modules
22//!
23//! - `app`: Contains the core application logic for the Rust Assistant.
24//! - `cache`: Provides caching functionalities for crates.
25//! - `download`: Handles the downloading of crates and their contents.
26//! - `search`: Implements search algorithms and data structures for efficient crate content search.
27//!
28pub mod app;
29
30#[cfg(feature = "axum")]
31pub mod axum;
32pub mod cache;
33pub mod download;
34pub mod github;
35pub mod search;
36
37use serde::{Deserialize, Serialize};
38use std::collections::BTreeSet;
39use std::fmt::{Display, Formatter};
40use std::num::NonZeroUsize;
41use std::ops::{Range, RangeInclusive};
42use std::path::{Path, PathBuf};
43use std::sync::Arc;
44
45#[cfg(feature = "utoipa")]
46use utoipa::ToSchema;
47
48pub use app::*;
49pub use github::*;
50pub use search::*;
51
52/// Represents the name and version of a crate.
53///
54/// This struct is used to uniquely identify a crate with its name and version number.
55#[derive(Debug, Deserialize, Serialize, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
56pub struct CrateVersion {
57 /// The exact name of the crate
58 #[serde(rename = "crate")]
59 pub krate: Arc<str>,
60 /// The semantic version number of the specified crate, following the Semantic versioning specification.
61 pub version: Arc<str>,
62}
63
64impl CrateVersion {
65 /// Computes the root directory for the specified crate version.
66 ///
67 /// This method concatenates the crate name and version number to form a path-like string,
68 /// which can be used as a directory name to store crate-related data.
69 ///
70 pub fn root_dir(&self) -> PathBuf {
71 PathBuf::from(format!("{}-{}", self.krate, self.version))
72 }
73}
74
75impl<C, V> From<(C, V)> for CrateVersion
76where
77 C: AsRef<str>,
78 V: AsRef<str>,
79{
80 /// Creates a `CrateVersion` instance from a tuple of crate name and version.
81 ///
82 /// This method allows for a convenient way to construct a `CrateVersion` from separate
83 /// name and version strings.
84 fn from(value: (C, V)) -> Self {
85 Self {
86 krate: Arc::from(value.0.as_ref()),
87 version: Arc::from(value.1.as_ref()),
88 }
89 }
90}
91
92impl Display for CrateVersion {
93 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}-{}", self.krate, self.version)
95 }
96}
97
98/// Represents a path within a specific crate's directory structure.
99///
100/// It combines the crate version information with the relative path within the crate.
101#[derive(Debug, Deserialize, Serialize)]
102pub struct CrateVersionPath {
103 /// The name and version of a crate.
104 #[serde(flatten)]
105 pub crate_version: CrateVersion,
106 /// The path.
107 pub path: Arc<str>,
108}
109
110/// Represents a range of lines in a file.
111///
112/// This struct is used to specify a start and end line for operations that work with line ranges.
113#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
114pub struct FileLineRange {
115 /// The start line number.
116 pub start: Option<NonZeroUsize>,
117 /// The end line number.
118 pub end: Option<NonZeroUsize>,
119}
120
121/// Represents the contents of a directory, including files and subdirectories.
122///
123/// This is used to provide a snapshot of a directory's contents, listing all files and directories.
124#[derive(Debug, Deserialize, Serialize, Clone)]
125#[cfg_attr(feature = "utoipa", derive(ToSchema))]
126pub struct Directory {
127 /// Files in the directory.
128 #[cfg_attr(feature = "utoipa", schema(value_type = BTreeSet<String>))]
129 pub files: Arc<BTreeSet<PathBuf>>,
130 /// Subdirectories in the directory.
131 #[cfg_attr(feature = "utoipa", schema(value_type = BTreeSet<String>))]
132 pub directories: Arc<BTreeSet<PathBuf>>,
133}
134
135impl Directory {
136 /// Checks whether the directory is empty.
137 ///
138 /// This method returns `true` if both the `files` and `directories` sets are empty,
139 /// indicating that the directory has no contents.
140 pub fn is_empty(&self) -> bool {
141 self.files.is_empty() && self.directories.is_empty()
142 }
143}
144
145#[derive(Debug, Default)]
146pub struct DirectoryMut {
147 pub files: BTreeSet<PathBuf>,
148 pub directories: BTreeSet<PathBuf>,
149}
150
151impl DirectoryMut {
152 /// Checks whether the mutable directory is empty.
153 ///
154 /// Similar to `Directory::is_empty`, but for the mutable version of the directory.
155 /// It's useful for scenarios where directory contents are being modified.
156 pub fn is_empty(&self) -> bool {
157 self.files.is_empty() && self.directories.is_empty()
158 }
159
160 /// Freezes the directory, converting it into an immutable `Directory`.
161 ///
162 /// This method converts `DirectoryMut` into `Directory` by wrapping its contents
163 /// in `Arc`, thus allowing for safe shared access.
164 ///
165 pub fn freeze(self) -> Directory {
166 Directory {
167 files: Arc::new(self.files),
168 directories: Arc::new(self.directories),
169 }
170 }
171}
172
173/// Represents a query for searching items in a crate.
174///
175/// This struct is used to specify the criteria for searching items like structs, enums, traits, etc., within a crate.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[cfg_attr(feature = "utoipa", derive(ToSchema))]
178pub struct ItemQuery {
179 /// The type of item to search for.
180 #[serde(rename = "type")]
181 pub type_: ItemType,
182 /// The query string used for searching.
183 pub query: String,
184 /// Optional path within the crate to narrow down the search scope.
185 pub path: Option<PathBuf>,
186}
187
188/// Represents an item found in a crate.
189///
190/// This struct describes an item, such as a struct or function, including its location within the crate.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[cfg_attr(feature = "utoipa", derive(ToSchema))]
193pub struct Item {
194 /// The name of the item.
195 pub name: String,
196 /// The type of the item.
197 #[serde(rename = "type")]
198 pub type_: ItemType,
199 /// The file path where the item is located.
200 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
201 pub file: Arc<Path>,
202 /// The range of lines in the file where the item is defined.
203 #[cfg_attr(feature = "utoipa", schema(value_type = RangeSchema))]
204 pub line_range: RangeInclusive<NonZeroUsize>,
205}
206
207/// Defines various types of items that can be searched for in a crate.
208///
209/// This enum lists different types of code constructs like structs, enums, traits, etc.
210#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
211#[serde(rename_all = "kebab-case")]
212#[cfg_attr(feature = "utoipa", derive(ToSchema))]
213pub enum ItemType {
214 /// Represents all item types.
215 #[default]
216 All,
217 /// A struct definition.
218 Struct,
219 /// An enum definition.
220 Enum,
221 /// A trait definition.
222 Trait,
223 /// Type implementation.
224 ImplType,
225 /// Trait implementation for a type.
226 ImplTraitForType,
227 /// A macro definition.
228 Macro,
229 /// An attribute macro.
230 AttributeMacro,
231 /// A standalone function.
232 Function,
233 /// A type alias.
234 TypeAlias,
235}
236
237/// Represents a query for searching lines within files in a crate.
238///
239/// This struct is used for specifying criteria for line-based searches, such as finding specific text within files.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[cfg_attr(feature = "utoipa", derive(ToSchema))]
242pub struct LineQuery {
243 /// The text or pattern to search for.
244 pub query: String,
245 /// The search mode (e.g., plain text or regular expression).
246 pub mode: SearchMode,
247 /// Indicates if the search should be case-sensitive.
248 #[serde(default)]
249 pub case_sensitive: bool,
250 /// Indicates if the search should match whole words only.
251 #[serde(default)]
252 pub whole_word: bool,
253 /// The maximum number of results to return.
254 #[cfg_attr(feature = "utoipa", schema(value_type = usize))]
255 pub max_results: Option<NonZeroUsize>,
256 /// A comma-separated string specifying file extensions to include in the search.
257 /// Each segment represents a file extension, e.g., "rs,txt" for Rust and text files.
258 #[serde(default)]
259 pub file_ext: String,
260 /// Optional path within the crate to limit the search scope.
261 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
262 pub path: Option<PathBuf>,
263}
264
265/// Defines different modes for searching text.
266///
267/// This enum distinguishes between plain text searches and regular expression searches.
268#[derive(Debug, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)]
269#[cfg_attr(feature = "utoipa", derive(ToSchema))]
270#[serde(rename_all = "kebab-case")]
271pub enum SearchMode {
272 /// A plain text search.
273 PlainText,
274 /// A regular expression search.
275 Regex,
276}
277
278/// Represents a specific line found in a search operation.
279///
280/// This struct contains details about a line of text found in a file, including its content and location.
281#[derive(Debug, Serialize, Deserialize)]
282#[cfg_attr(feature = "utoipa", derive(ToSchema))]
283pub struct Line {
284 /// The content of the line.
285 pub line: String,
286 /// The file path where the line is located.
287 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
288 pub file: PathBuf,
289 /// The line number within the file.
290 #[cfg_attr(feature = "utoipa", schema(value_type = usize))]
291 pub line_number: NonZeroUsize,
292 /// The range of columns in the line where the text was found.
293 #[cfg_attr(feature = "utoipa", schema(value_type = RangeSchema))]
294 pub column_range: Range<NonZeroUsize>,
295}
296
297/// Schema for representing a range, used in other structs to describe line and column ranges.
298#[cfg(feature = "utoipa")]
299#[derive(ToSchema)]
300pub struct RangeSchema {
301 /// The start line number.
302 pub start: usize,
303 /// The end line number.
304 pub end: usize,
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::cache::{Crate, CrateCache, CrateTar};
311 use crate::download::CrateDownloader;
312 use std::num::NonZeroUsize;
313
314 #[tokio::test]
315 async fn download_and_read() -> anyhow::Result<()> {
316 // let start = Instant::now();
317 let crate_version = CrateVersion::from(("tokio", "1.35.1"));
318 let downloader = CrateDownloader::default();
319 let tar_data = downloader.download_crate_file(&crate_version).await?;
320 let cache = CrateCache::new(NonZeroUsize::new(1024).unwrap());
321 let crate_tar = CrateTar::from((crate_version.clone(), tar_data));
322 let krate = Crate::try_from(crate_tar)?;
323 let old = cache.set_crate(crate_version.clone(), krate);
324 assert!(old.is_none());
325
326 let crate_ = cache.get_crate(&crate_version).expect("get crate");
327
328 let files = crate_.read_directory("").expect("read directory");
329 assert!(!files.is_empty());
330 println!("{:#?}", files);
331
332 let lib_rs_content = crate_.get_file_by_line_range("src/lib.rs", ..)?;
333 assert!(lib_rs_content.is_some());
334 let lib_rs_range_content =
335 crate_.get_file_by_line_range("src/lib.rs", ..NonZeroUsize::new(27).unwrap())?;
336 assert!(lib_rs_range_content.is_some());
337 // println!("{}", lib_rs_range_content.expect("lib.rs"));
338 // println!("Elapsed: {}µs", start.elapsed().as_micros());
339
340 let file = crate_
341 .get_file_by_line_range("src/lib.rs", ..=NonZeroUsize::new(3).unwrap())?
342 .unwrap();
343 println!("[{}]", std::str::from_utf8(file.data.as_ref()).unwrap());
344
345 let lines = crate_.search_line(&LineQuery {
346 query: "Sleep".to_string(),
347 mode: SearchMode::PlainText,
348 case_sensitive: true,
349 whole_word: true,
350 max_results: Some(6.try_into().expect("6")),
351 file_ext: "rs".into(),
352 path: Some(PathBuf::from("src")),
353 })?;
354 println!("{:#?}", lines);
355 Ok(())
356 }
357}