willbe/action/
crate_doc.rs

1// module/move/willbe/src/action/crate_doc.rs
2mod private
3{
4  #[ allow( clippy::wildcard_imports ) ]
5  use crate::*;
6
7  use process_tools::process;
8  use error::
9  {
10    untyped::Context,
11    typed::Error,
12    ErrWith,
13  };
14  use core::fmt;
15  use std::
16  {
17    ffi::OsString,
18    fs,
19    path::PathBuf,
20  };
21  use collection_tools::HashMap;
22  use toml_edit::Document;
23  use rustdoc_md::rustdoc_json_types::Crate as RustdocCrate;
24  use rustdoc_md::rustdoc_json_to_markdown;
25
26  /// Represents errors specific to the crate documentation generation process.
27  #[ derive( Debug, Error ) ]
28  pub enum CrateDocError
29  {
30    /// Error related to file system operations (reading/writing files).
31    #[ error( "I/O error: {0}" ) ]
32    Io( #[ from ] std::io::Error ),
33    /// Error encountered while parsing the Cargo.toml file.
34    #[ error( "Failed to parse Cargo.toml: {0}" ) ]
35    Toml( #[ from ] toml_edit::TomlError ),
36    /// Error occurred during the execution of the `cargo doc` command.
37    #[ error( "Failed to execute cargo doc command: {0}" ) ]
38    Command( String ),
39    /// Error encountered while deserializing the JSON output from `cargo doc`.
40    #[ error( "Failed to deserialize rustdoc JSON: {0}" ) ]
41    Json( #[ from ] serde_json::Error ),
42    /// Error occurred during the conversion from JSON to Markdown.
43    #[ error( "Failed to render Markdown: {0}" ) ]
44    MarkdownRender( String ),
45    /// The package name could not be found within the Cargo.toml file.
46    #[ error( "Missing package name in Cargo.toml at {0}" ) ]
47    MissingPackageName( PathBuf ),
48    /// The JSON documentation file generated by `cargo doc` was not found.
49    #[ error( "Generated JSON documentation file not found at {0}" ) ]
50    JsonFileNotFound( PathBuf ),
51    /// Error related to path manipulation or validation.
52    #[ error( "Path error: {0}" ) ]
53    Path( #[ from ] PathError ),
54    /// A general, untyped error occurred.
55    #[ error( "Untyped error: {0}" ) ]
56    Untyped( #[ from ] error::untyped::Error ),
57  }
58
59  /// Report detailing the outcome of the documentation generation.
60  #[ derive( Debug, Default, Clone ) ]
61  pub struct CrateDocReport
62  {
63    /// The directory of the crate processed.
64    pub crate_dir : Option< CrateDir >,
65    /// The path where the Markdown file was (or was attempted to be) written.
66    pub output_path : Option< PathBuf >,
67    /// A summary status message of the operation.
68    pub status : String,
69    /// Output of the cargo doc command, if executed.
70    pub cargo_doc_report : Option< process::Report >,
71  }
72
73  impl fmt::Display for CrateDocReport
74  {
75    fn fmt( &self, f : &mut fmt::Formatter< '_ > ) -> fmt::Result
76    {
77      // Status is the primary message
78      writeln!( f, "{}", self.status )?;
79      // Add crate and output path details for context
80      if let Some( crate_dir ) = &self.crate_dir
81      {
82        writeln!( f, "  Crate: {}", crate_dir.as_ref().display() )?;
83      }
84      if let Some( output_path ) = &self.output_path
85      {
86        writeln!( f, "  Output: {}", output_path.display() )?;
87      }
88      Ok( () )
89    }
90  }
91
92  ///
93  /// Generate documentation for a crate in a single Markdown file.
94  /// Executes `cargo doc` to generate JSON output, reads the JSON,
95  /// uses `rustdoc-md` to convert it to Markdown, and saves the result.
96  ///
97  /// # Arguments
98  /// * `workspace` - A reference to the workspace containing the crate.
99  /// * `crate_dir` - The directory of the crate for which to generate documentation.
100  /// * `output_path_req` - Optional path for the output Markdown file.
101  ///
102  /// # Returns
103  /// Returns `Ok(CrateDocReport)` if successful, otherwise returns `Err((CrateDocReport, CrateDocError))`.
104  /// 
105  /// # Errors
106  /// Returns an error if the command arguments are invalid, the workspace cannot be loaded
107  #[ allow( clippy::too_many_lines, clippy::result_large_err ) ]
108  pub fn doc
109  (
110    workspace : &Workspace,
111    crate_dir : &CrateDir,
112    output_path_req : Option< PathBuf >,
113  ) -> ResultWithReport< CrateDocReport, CrateDocError >
114  {
115    let mut report = CrateDocReport 
116    {
117      crate_dir : Some( crate_dir.clone() ),
118      status : format!( "Starting documentation generation for {}", crate_dir.as_ref().display() ),
119      ..Default::default()
120    };
121  
122
123    // --- Get crate name early for --package argument and file naming ---
124    let manifest_path_for_name = crate_dir.as_ref().join( "Cargo.toml" );
125    let manifest_content_for_name = fs::read_to_string( &manifest_path_for_name )
126    .map_err( CrateDocError::Io )
127    .context( format!( "Failed to read Cargo.toml at {}", manifest_path_for_name.display() ) )
128    .err_with_report( &report )?;
129    let manifest_toml_for_name = manifest_content_for_name.parse::< Document >()
130    .map_err( CrateDocError::Toml )
131    .context( format!( "Failed to parse Cargo.toml at {}", manifest_path_for_name.display() ) )
132    .err_with_report( &report )?;
133    let crate_name = manifest_toml_for_name[ "package" ][ "name" ]
134    .as_str()
135    .ok_or_else( || CrateDocError::MissingPackageName( manifest_path_for_name.clone() ) )
136    .err_with_report( &report )?;
137    // --- End get crate name early ---
138
139    // Define the arguments for `cargo doc`
140    let args: Vec< OsString > = vec!
141    [
142      "doc".into(),
143      "--no-deps".into(),
144      "--package".into(),
145      crate_name.into(),
146    ];
147
148    // Define environment variables
149    let envs: HashMap< String, String > =
150    [
151      ( "RUSTC_BOOTSTRAP".to_string(), "1".to_string() ),
152      ( "RUSTDOCFLAGS".to_string(), "-Z unstable-options --output-format json".to_string() ),
153    ].into();
154
155    // Execute the command from the workspace root
156    let cargo_report_result = process::Run::former()
157    .bin_path( "cargo" )
158    .args( args )
159    .current_path( workspace.workspace_root().absolute_path() )
160    .env_variable( envs )
161    .run();
162
163    // Store report regardless of outcome and update status if it failed
164    match &cargo_report_result
165    {
166      Ok( r ) => report.cargo_doc_report = Some( r.clone() ),
167      Err( r ) =>
168      {
169        report.cargo_doc_report = Some( r.clone() );
170        report.status = format!( "Failed during `cargo doc` execution for `{crate_name}`." );
171      }
172    }
173
174    // Handle potential command execution error using err_with_report
175    let _cargo_report = cargo_report_result
176      .map_err( | report | CrateDocError::Command( report.to_string() ) )
177      .err_with_report( &report )?;
178
179    // Construct path to the generated JSON file using workspace target dir
180    let json_path = workspace
181      .target_directory()
182      .join( "doc" )
183      .join( format!( "{crate_name}.json" ) );
184
185    // Check if JSON file exists and read it
186    if !json_path.exists()
187    {
188      report.status = format!( "Generated JSON documentation file not found at {}", json_path.display() );
189      return Err(( report, CrateDocError::JsonFileNotFound( json_path ) ));
190    }
191    let json_content = fs::read_to_string( &json_path )
192      .map_err( CrateDocError::Io )
193      .context( format!( "Failed to read JSON documentation file at {}", json_path.display() ) )
194      .err_with_report( &report )?;
195
196    // Deserialize JSON content into RustdocCrate struct
197    let rustdoc_crate: RustdocCrate = serde_json::from_str( &json_content )
198      .map_err( CrateDocError::Json )
199      .context( format!( "Failed to deserialize JSON from {}", json_path.display() ) )
200      .err_with_report( &report )?;
201
202    // Define output Markdown file path
203    let output_md_abs_path = match output_path_req
204    {
205      // If a path was provided
206      Some( req_path ) =>
207      {
208        if req_path.is_absolute()
209        {
210          // Use it directly if absolute
211          req_path
212        }
213        else
214        {
215          // Resolve relative to CWD if relative
216          std::env::current_dir()
217            .map_err( CrateDocError::Io )
218            .context( "Failed to get current directory to resolve output path" )
219            .err_with_report( &report )?
220            .join( req_path )
221            // Removed canonicalize call here
222        }
223      }
224      // If no path was provided, default to workspace target/doc directory
225      None =>
226      {
227        workspace
228          .target_directory()
229          .join( "doc" )
230          .join( format!( "{crate_name}_doc.md" ) )
231      }
232    };
233
234    report.output_path = Some( output_md_abs_path.clone() );
235
236    // Use rustdoc_json_to_markdown to convert the Crate struct to Markdown string
237    let markdown_content = rustdoc_json_to_markdown( rustdoc_crate );
238
239    // Write the Markdown string to the output file
240    if let Some( parent_dir ) = output_md_abs_path.parent()
241    {
242      fs::create_dir_all( parent_dir )
243        .map_err( CrateDocError::Io )
244        .context( format!( "Failed to create output directory {}", parent_dir.display() ) )
245        .err_with_report( &report )?;
246    }
247    fs::write( &output_md_abs_path, markdown_content )
248      .map_err( CrateDocError::Io )
249      .context( format!( "Failed to write Markdown documentation to {}", output_md_abs_path.display() ) )
250      .err_with_report( &report )?;
251
252    report.status = format!( "Markdown documentation generated successfully for `{crate_name}`" );
253
254    Ok( report )
255  }
256}
257
258crate::mod_interface!
259{
260  /// Generate documentation action.
261  orphan use doc;
262  /// Report for documentation generation.
263  orphan use CrateDocReport;
264  /// Error type for documentation generation.
265  orphan use CrateDocError;
266}