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}