willbe 0.35.0

Utility to publish multi-crate and multi-workspace environments and maintain their consistency.
Documentation
// module/experimental/willbe/src/action/crate_doc.rs
mod private
{

  use crate :: *;

  use process_tools ::process;
  use error ::
  {
  untyped ::Context,
  typed ::Error,
  ErrWith,
 };
  use core ::fmt;
  use std ::
  {
  ffi ::OsString,
  fs,
  path ::PathBuf,
 };
  use toml_edit ::Document;
  use rustdoc_md ::rustdoc_json_types ::Crate as RustdocCrate;
  use rustdoc_md ::rustdoc_json_to_markdown;
  // Explicit import for Result and its variants for pattern matching
  use core ::result ::Result :: { Ok, Err };

  /// Represents errors specific to the crate documentation generation process.
  #[ derive( Debug, Error ) ]
  pub enum CrateDocError
  {
  /// Error related to file system operations (reading/writing files).
  #[ error( "I/O error: {0}" ) ]
  Io( #[ from ] std ::io ::Error ),
  /// Error encountered while parsing the Cargo.toml file.
  #[ error( "Failed to parse Cargo.toml: {0}" ) ]
  Toml( #[ from ] toml_edit ::TomlError ),
  /// Error occurred during the execution of the `cargo doc` command.
  #[ error( "Failed to execute cargo doc command: {0}" ) ]
  Command( String ),
  /// Error encountered while deserializing the JSON output from `cargo doc`.
  #[ error( "Failed to deserialize rustdoc JSON: {0}" ) ]
  Json( #[ from ] serde_json ::Error ),
  /// Error occurred during the conversion from JSON to Markdown.
  #[ error( "Failed to render Markdown: {0}" ) ]
  MarkdownRender( String ),
  /// The package name could not be found within the Cargo.toml file.
  #[ error( "Missing package name in Cargo.toml at {0}" ) ]
  MissingPackageName( PathBuf ),
  /// The JSON documentation file generated by `cargo doc` was not found.
  #[ error( "Generated JSON documentation file not found at {0}" ) ]
  JsonFileNotFound( PathBuf ),
  /// Error related to path manipulation or validation.
  #[ error( "Path error: {0}" ) ]
  Path( #[ from ] PathError ),
  /// A general, untyped error occurred.
  #[ error( "Untyped error: {0}" ) ]
  Untyped( #[ from ] error ::untyped ::Error ),
 }

  /// Report detailing the outcome of the documentation generation.
  #[ derive( Debug, Default, Clone ) ]
  pub struct CrateDocReport
  {
  /// The directory of the crate processed.
  pub crate_dir: Option< CrateDir >,
  /// The path where the Markdown file was (or was attempted to be) written.
  pub output_path: Option< PathBuf >,
  /// A summary status message of the operation.
  pub status: String,
  /// Output of the cargo doc command, if executed.
  pub cargo_doc_report: Option< process ::Report >,
 }

  impl fmt ::Display for CrateDocReport
  {
  fn fmt( &self, f: &mut fmt ::Formatter< '_ > ) -> fmt ::Result
  {
   // Status is the primary message
   writeln!( f, "{}", self.status )?;
   // Add crate and output path details for context
   if let Some( crate_dir ) = &self.crate_dir
   {
  writeln!( f, "  Crate: {}", crate_dir.as_ref().display() )?;
 }
   if let Some( output_path ) = &self.output_path
   {
  writeln!( f, "  Output: {}", output_path.display() )?;
 }
   Ok( () )
 }
 }

  ///
  /// Generate documentation for a crate in a single Markdown file.
  /// Executes `cargo doc` to generate JSON output, reads the JSON,
  /// uses `rustdoc-md` to convert it to Markdown, and saves the result.
  ///
  /// # Arguments
  /// * `workspace` - A reference to the workspace containing the crate.
  /// * `crate_dir` - The directory of the crate for which to generate documentation.
  /// * `output_path_req` - Optional path for the output Markdown file.
  ///
  /// # Returns
  /// Returns `Ok(CrateDocReport)` if successful, otherwise returns `Err((CrateDocReport, CrateDocError))`.
  ///
  /// # Errors
  /// Returns an error if the command arguments are invalid, the workspace cannot be loaded
  #[ allow( clippy ::too_many_lines, clippy ::result_large_err ) ]
  pub fn doc
  (
  workspace: &Workspace,
  crate_dir: &CrateDir,
  output_path_req: Option< PathBuf >,
 ) -> ResultWithReport< CrateDocReport, CrateDocError >
  {
  let mut report = CrateDocReport
  {
   crate_dir: Some( crate_dir.clone() ),
   status: format!( "Starting documentation generation for {}", crate_dir.as_ref().display() ),
   ..Default ::default()
 };


  // --- Get crate name early for --package argument and file naming ---
  let manifest_path_for_name = crate_dir.as_ref().join( "Cargo.toml" );
  let manifest_content_for_name = fs ::read_to_string( &manifest_path_for_name )
  .map_err( CrateDocError ::Io )
  .context( format!( "Failed to read Cargo.toml at {}", manifest_path_for_name.display() ) )
  .err_with_report( &report )?;
  let manifest_toml_for_name = manifest_content_for_name.parse :: < Document >()
  .map_err( CrateDocError ::Toml )
  .context( format!( "Failed to parse Cargo.toml at {}", manifest_path_for_name.display() ) )
  .err_with_report( &report )?;
  let crate_name = manifest_toml_for_name[ "package" ][ "name" ]
  .as_str()
  .ok_or_else( || CrateDocError ::MissingPackageName( manifest_path_for_name.clone() ) )
  .err_with_report( &report )?;
  // --- End get crate name early ---

  // Define the arguments for `cargo doc`
  let args: Vec< OsString > = vec!
  [
   "doc".into(),
   "--no-deps".into(),
   "--package".into(),
   crate_name.into(),
 ];

  // Define environment variables
  let envs: std ::collections ::HashMap< String, String > =
  [
   ( "RUSTC_BOOTSTRAP".to_string(), "1".to_string() ),
   ( "RUSTDOCFLAGS".to_string(), "-Z unstable-options --output-format json".to_string() ),
 ].into();

  // Execute the command from the workspace root
  let cargo_report_result = process ::Run ::former()
  .bin_path( "cargo" )
  .args( args )
  .current_path( workspace.workspace_root().absolute_path() )
  .env_variable( envs )
  .run();

  // Store report regardless of outcome and update status if it failed
  match &cargo_report_result
  {
   Ok( r ) => report.cargo_doc_report = Some( r.clone() ),
   Err( r ) =>
   {
  report.cargo_doc_report = Some( r.clone() );
  report.status = format!( "Failed during `cargo doc` execution for `{crate_name}`." );
 }
 }

  // Handle potential command execution error using err_with_report
  let _cargo_report = cargo_report_result
   .map_err( | report | CrateDocError ::Command( report.to_string() ) )
   .err_with_report( &report )?;

  // Construct path to the generated JSON file using workspace target dir
  let json_path = workspace
   .target_directory()
   .join( "doc" )
   .join( format!( "{crate_name}.json" ) );

  // Check if JSON file exists and read it
  if !json_path.exists()
  {
   report.status = format!( "Generated JSON documentation file not found at {}", json_path.display() );
   return Err(( report, CrateDocError ::JsonFileNotFound( json_path ) ));
 }
  let json_content = fs ::read_to_string( &json_path )
   .map_err( CrateDocError ::Io )
   .context( format!( "Failed to read JSON documentation file at {}", json_path.display() ) )
   .err_with_report( &report )?;

  // Deserialize JSON content into RustdocCrate struct
  let rustdoc_crate: RustdocCrate = serde_json ::from_str( &json_content )
   .map_err( CrateDocError ::Json )
   .context( format!( "Failed to deserialize JSON from {}", json_path.display() ) )
   .err_with_report( &report )?;

  // Define output Markdown file path
  let output_md_abs_path = match output_path_req
  {
   // If a path was provided
   Some( req_path ) =>
   {
  if req_path.is_absolute()
  {
   // Use it directly if absolute
   req_path
 }
  else
  {
   // Resolve relative to CWD if relative
   std ::env ::current_dir()
  .map_err( CrateDocError ::Io )
  .context( "Failed to get current directory to resolve output path" )
  .err_with_report( &report )?
  .join( req_path )
  // Removed canonicalize call here
 }
 }
   // If no path was provided, default to workspace target/doc directory
   None =>
   {
  workspace
   .target_directory()
   .join( "doc" )
   .join( format!( "{crate_name}_doc.md" ) )
 }
 };

  report.output_path = Some( output_md_abs_path.clone() );

  // Use rustdoc_json_to_markdown to convert the Crate struct to Markdown string
  let markdown_content = rustdoc_json_to_markdown( rustdoc_crate );

  // Write the Markdown string to the output file
  if let Some( parent_dir ) = output_md_abs_path.parent()
  {
   fs ::create_dir_all( parent_dir )
  .map_err( CrateDocError ::Io )
  .context( format!( "Failed to create output directory {}", parent_dir.display() ) )
  .err_with_report( &report )?;
 }
  fs ::write( &output_md_abs_path, markdown_content )
   .map_err( CrateDocError ::Io )
   .context( format!( "Failed to write Markdown documentation to {}", output_md_abs_path.display() ) )
   .err_with_report( &report )?;

  report.status = format!( "Markdown documentation generated successfully for `{crate_name}`" );

  Ok( report )
 }
}

crate ::mod_interface!
{
  /// Generate documentation action.
  orphan use doc;
  /// Report for documentation generation.
  orphan use CrateDocReport;
  /// Error type for documentation generation.
  orphan use CrateDocError;
}