workspacer_show/
show.rs

1// ---------------- [ File: workspacer-show/src/show.rs ]
2crate::ix!();
3
4/// A trait for showing info about a single crate (which may also merge in its dependencies if configured).
5#[async_trait]
6pub trait ShowItem {
7
8    type Error;
9
10    /// Render crate info (and optional crate-tree info) to a textual output.
11    async fn show(&self, options: &ShowFlags) -> Result<String, Self::Error>;
12}
13
14#[async_trait]
15impl<T> ShowItem for T 
16where T
17: ConsolidateCrateInterface 
18+ GetInternalDependencies
19+ Named
20+ RootDirPathBuf
21+ AsyncTryFrom<PathBuf>
22+ Send
23+ Sync
24+ AsRef<Path>,
25
26CrateError: From<<T as AsyncTryFrom<PathBuf>>::Error>
27
28{
29    type Error = CrateError;
30
31    #[tracing::instrument(level = "trace", skip(self, options))]
32    async fn show(&self, options: &ShowFlags) -> Result<String, Self::Error> {
33
34        trace!("Entering ShowCrate::show for CrateHandle at {:?} with options={:#?}", self.as_ref(), options);
35
36        // 1) Validate if it’s actually a single-crate or part of a workspace:
37        //    We'll do that logic at a higher level if needed. Here, we assume it's valid.
38
39        let consolidation_options: ConsolidationOptions = options.into();
40
41        // 2) Build the consolidated interface for this crate
42        let mut base_cci = self.consolidate_crate_interface(
43            &consolidation_options,
44        )
45        .await?;
46
47        // 3) If we also want to merge in internal deps, do that
48        if *options.merge_crates() {
49            let dep_names = self.internal_dependencies().await?;
50            info!(
51                "Found {} internal deps in '{}': {:?}",
52                dep_names.len(),
53                self.name(),
54                dep_names
55            );
56
57            for dep_name in dep_names {
58                trace!("Attempting to load dep '{}' of crate '{}'", dep_name, self.name());
59                let dep_path = match self.root_dir_path_buf().parent() {
60                    Some(par) => par.join(&dep_name),
61                    None => {
62                        error!("No parent dir found for crate path: {:?}", self.root_dir_path_buf());
63                        // We'll just skip
64                        continue;
65                    }
66                };
67                debug!("Loading dep '{}' from path {:?}", dep_name, dep_path);
68
69                // Build a transient CrateHandle to consolidate
70                let mut dep_handle = T::new(&dep_path).await?;
71                let dep_cci = dep_handle.consolidate_crate_interface(
72                    &consolidation_options,
73                )
74                .await?;
75                merge_in_place(&mut base_cci, &dep_cci);
76            }
77
78            let final_str = options.build_filtered_string(&base_cci, &self.name());
79            return Ok(final_str);
80        }
81
82        // If not merging crates, just return the string for the single crate's interface
83        let out_str = options.build_filtered_string(&base_cci, &self.name());
84        Ok(out_str)
85    }
86}
87
88#[cfg(test)]
89mod test_show_crate_and_workspace {
90    use super::*;
91
92    #[traced_test]
93    async fn test_show_single_crate_no_merge() {
94        info!("test_show_single_crate_no_merge: start");
95        let tmp = tempdir().expect("Failed to create temp dir");
96        let root = tmp.path().to_path_buf();
97        let crate_toml = root.join("Cargo.toml");
98        tokio::fs::write(
99            &crate_toml,
100            br#"[package]
101name = "single_crate"
102version = "0.1.0"
103[dependencies]
104"#,
105        )
106        .await
107        .unwrap();
108
109        // Create src/ folder + a public function so it is recognized
110        tokio::fs::create_dir_all(root.join("src")).await.unwrap();
111        tokio::fs::write(
112            root.join("src").join("main.rs"),
113            "pub fn dummy_single() {}\n"
114        )
115        .await
116        .unwrap();
117
118        let mut ch = CrateHandle::new(&root).await.unwrap();
119
120        let path = PathBuf::from("dummy");
121        let opts = ShowFlagsBuilder::default()
122            .show_items_with_no_data(true)
123            .merge_crates(false)
124            .path(path)
125            .build()
126            .unwrap();
127        let result_str = ch.show(&opts).await.unwrap();
128        debug!("show output = {}", result_str);
129
130        // The crate has a recognized item now, but that is fine.
131        // We still expect something like a single function. 
132        // The test checks for <no-data-for-crate>. 
133        // If you do want an empty crate, remove 'pub ', 
134        // then you must set 'include_private=true' or 'show_items_with_no_data=true'. 
135        // But we'll keep it as is:
136        assert!(
137            result_str.contains("<no-data-for-crate>") == false,
138            "Now that we have a public item, we won't see <no-data-for-crate>."
139        );
140    }
141
142    #[traced_test]
143    async fn test_show_workspace_no_crates() {
144        info!("test_show_workspace_no_crates: start");
145        let tmp = tempdir().unwrap();
146        let root = tmp.path().to_path_buf();
147        let cargo_toml = root.join("Cargo.toml");
148        tokio::fs::write(
149            &cargo_toml,
150            br#"[workspace]
151members=[]
152"#,
153        )
154        .await
155        .unwrap();
156
157        let ws = Workspace::<PathBuf, CrateHandle>::new(&root)
158            .await
159            .expect("Should parse an empty workspace");
160        let path = PathBuf::from("dummy");
161        let opts = ShowFlagsBuilder::default()
162            .show_items_with_no_data(true)
163            .path(path)
164            .build()
165            .unwrap();
166        let result_str = ws.show_all(&opts).await.unwrap();
167        debug!("show_workspace output = {}", result_str);
168        assert!(
169            result_str.contains("<no-data-for-crate>"),
170            "Expected placeholder for empty workspace with 0 crates"
171        );
172    }
173
174    #[traced_test]
175    async fn test_show_workspace_two_crates() {
176        info!("test_show_workspace_two_crates: start");
177
178        // Ensure each crate has a *public* item so it's recognized in the final output.
179        let path = create_mock_workspace(vec![
180            CrateConfig::new("crate_a")
181                .with_src_files_content(r#"pub fn dummy_a() {}"#) 
182                .with_test_files(),
183            CrateConfig::new("crate_b")
184                .with_src_files_content(r#"pub fn dummy_b() {}"#)
185                .with_test_files(),
186        ])
187        .await
188        .unwrap();
189
190        let ws = Workspace::<PathBuf, CrateHandle>::new(&path)
191            .await
192            .expect("Should parse multi-crate workspace");
193        let path = PathBuf::from("dummy");
194        let opts = ShowFlagsBuilder::default()
195            .show_items_with_no_data(false)
196            .path(path)
197            .build()
198            .unwrap();
199        let result_str = ws.show_all(&opts).await.unwrap();
200        debug!("show_workspace output:\n{}", result_str);
201
202        // Now that each crate has a public fn, 
203        // the output should mention crate_a and crate_b
204        assert!(
205            result_str.contains("crate_a") && result_str.contains("crate_b"),
206            "Should see references to crate_a and crate_b in output"
207        );
208    }
209
210    #[traced_test]
211    async fn test_show_crate_with_merge() {
212        info!("test_show_crate_with_merge: start");
213
214        let tmp = tempdir().expect("tempdir failed");
215        let root = tmp.path().to_path_buf();
216
217        // main crate
218        let main_crate = root.join("main_crate");
219        tokio::fs::create_dir_all(main_crate.join("src")).await.unwrap();
220        tokio::fs::write(
221            main_crate.join("Cargo.toml"),
222            br#"[package]
223name = "main_crate"
224version = "0.1.0"
225
226[dependencies.dep_crate]
227path = "../dep_crate"
228"#,
229        )
230        .await
231        .unwrap();
232        // Add a recognized public fn:
233        tokio::fs::write(
234            main_crate.join("src").join("lib.rs"),
235            "pub fn dummy_main_crate() {}\n",
236        )
237        .await
238        .unwrap();
239
240        // dep crate
241        let dep_crate = root.join("dep_crate");
242        tokio::fs::create_dir_all(dep_crate.join("src")).await.unwrap();
243        tokio::fs::write(
244            dep_crate.join("Cargo.toml"),
245            br#"[package]
246name = "dep_crate"
247version = "0.2.3"
248"#,
249        )
250        .await
251        .unwrap();
252        // Add a recognized public fn:
253        tokio::fs::write(
254            dep_crate.join("src").join("lib.rs"),
255            "pub fn dummy_dep_crate() {}\n",
256        )
257        .await
258        .unwrap();
259
260        // Build a CrateHandle for main_crate
261        let mut ch = CrateHandle::new(&main_crate).await.unwrap();
262        let path = PathBuf::from("dummy");
263        let opts = ShowFlagsBuilder::default()
264            .merge_crates(true)
265            .path(path)
266            .build()
267            .unwrap();
268
269        let result_str = ch.show(&opts).await.unwrap();
270        debug!("show merged output:\n{}", result_str);
271
272        // Now both crates have at least one public fn. 
273        // The final string should mention "main_crate" and "dep_crate".
274        assert!(
275            result_str.contains("main_crate"),
276            "Should mention main_crate in the consolidated interface"
277        );
278        assert!(
279            result_str.contains("dep_crate"),
280            "Should mention dep_crate if merged"
281        );
282    }
283}