diff --git a/module/move/willbe/src/action/main_header.rs b/module/move/willbe/src/action/main_header.rs index 253403d86c..e1b5a7a16e 100644 --- a/module/move/willbe/src/action/main_header.rs +++ b/module/move/willbe/src/action/main_header.rs @@ -1,6 +1,7 @@ mod private { use crate::*; + use std::fmt::{ Display, Formatter }; use std::fs:: { OpenOptions @@ -12,9 +13,8 @@ mod private SeekFrom, Write }; + use std::path::PathBuf; use regex::Regex; - use wtools::error::err; - use error_tools::Result; use wca::wtools::anyhow::Error; use action::readme_health_table_renew:: { @@ -23,12 +23,24 @@ mod private }; use _path::AbsolutePath; use { CrateDir, query, url, Workspace, wtools }; - use error_tools::for_app::Context; - use wtools::error::anyhow:: + use entity::{ CrateDirError, WorkspaceError }; + use wtools::error:: { - format_err + anyhow::format_err, + err, + for_app:: + { + Result, + Error as wError, + Context, + }, }; - + use error_tools:: + { + dependency::*, + for_lib::Error, + }; + static TAGS_TEMPLATE : std::sync::OnceLock< Regex > = std::sync::OnceLock::new(); fn regexes_initialize() @@ -36,6 +48,58 @@ mod private TAGS_TEMPLATE.set( Regex::new( r"(.|\n|\r\n)+" ).unwrap() ).ok(); } + /// Report. + #[ derive( Debug, Default, Clone ) ] + pub struct MainHeaderRenewReport + { + found_file : Option< PathBuf >, + touched_file : PathBuf, + success : bool, + } + + impl Display for MainHeaderRenewReport + { + fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result + { + if self.success + { + if let Some( file_path ) = self.touched_file.to_str() + { + writeln!( f, "File successful changed : {file_path}." )?; + } + else + { + writeln!( f, "File successful changed but contains non-UTF-8 characters." )?; + } + } + else + { + if let Some( Some( file_path ) ) = self.found_file.as_ref().map( | p | p.to_str() ) + { + writeln!( f, "File found but not changed : {file_path}." )?; + } + else + { + writeln!( f, "File not found or contains non-UTF-8 characters." )?; + } + } + Ok( () ) + } + } + + #[ derive( Debug, Error ) ] + pub enum MainHeaderRenewError + { + #[ error( "Common error: {0}" ) ] + Common(#[ from ] wError ), + #[ error( "I/O error: {0}" ) ] + IO( #[ from ] std::io::Error ), + #[ error( "Workspace error: {0}" ) ] + Workspace( #[ from ] WorkspaceError), + #[ error( "Directory error: {0}" ) ] + Directory( #[ from ] CrateDirError ), + } + /// The `HeaderParameters` structure represents a set of parameters, used for creating url for header. struct HeaderParameters { @@ -48,7 +112,7 @@ mod private impl HeaderParameters { /// Create `HeaderParameters` instance from the folder where Cargo.toml is stored. - fn from_cargo_toml( workspace : Workspace ) -> Result< Self > + fn from_cargo_toml( workspace : Workspace ) -> Result< Self, MainHeaderRenewError > { let repository_url = workspace.repository_url()?.ok_or_else::< Error, _ >( || err!( "repo_url not found in workspace Cargo.toml" ) )?; let master_branch = workspace.master_branch()?.unwrap_or( "master".into() ); @@ -68,7 +132,7 @@ mod private } /// Convert `Self`to header. - fn to_header( self ) -> Result< String > + fn to_header( self ) -> Result< String, MainHeaderRenewError > { let discord = self.discord_url.map( | discord | format!( "\n[![discord](https://img.shields.io/discord/872391416519737405?color=eee&logo=discord&logoColor=eee&label=ask)]({discord})" ) @@ -114,21 +178,40 @@ mod private /// [![docs.rs](https://raster.shields.io/static/v1?label=docs&message=online&color=eee&logo=docsdotrs&logoColor=eee)](https://docs.rs/wtools) /// /// ``` - pub fn readme_header_renew( path : AbsolutePath ) -> Result< () > + pub fn readme_header_renew( path : AbsolutePath ) -> Result< MainHeaderRenewReport, ( MainHeaderRenewReport, MainHeaderRenewError ) > { + let mut report = MainHeaderRenewReport::default(); regexes_initialize(); - let mut cargo_metadata = Workspace::with_crate_dir( CrateDir::try_from( path )? )?; - let workspace_root = workspace_root( &mut cargo_metadata )?; - let header_param = HeaderParameters::from_cargo_toml( cargo_metadata )?; - let read_me_path = workspace_root.join( readme_path( &workspace_root ).ok_or_else( || format_err!( "Fail to find README.md" ) )?); + let mut cargo_metadata = Workspace::with_crate_dir + ( + CrateDir::try_from( path ) + .map_err( | e | ( report.clone(), e.into() ) )? + ).map_err( | e | ( report.clone(), e.into() ) )?; + + let workspace_root = workspace_root( &mut cargo_metadata ) + .map_err( | e | ( report.clone(), e.into() ) )?; + + let header_param = HeaderParameters::from_cargo_toml( cargo_metadata ) + .map_err( | e | ( report.clone(), e.into() ) )?; + + let read_me_path = workspace_root.join + ( + readme_path( &workspace_root ) + .ok_or_else( || format_err!( "Fail to find README.md" ) ) + .map_err( | e | ( report.clone(), e.into() ) )? + ); + + report.found_file = Some( read_me_path.clone() ); + let mut file = OpenOptions::new() .read( true ) .write( true ) - .open( &read_me_path )?; + .open( &read_me_path ) + .map_err( | e | ( report.clone(), e.into() ) )?; let mut content = String::new(); - file.read_to_string( &mut content )?; + file.read_to_string( &mut content ).map_err( | e | ( report.clone(), e.into() ) )?; let raw_params = TAGS_TEMPLATE .get() @@ -140,12 +223,19 @@ mod private _ = query::parse( raw_params ).context( "Fail to parse arguments" ); - let header = header_param.to_header()?; - let content : String = TAGS_TEMPLATE.get().unwrap().replace( &content, &format!( "\n{header}\n" ) ).into(); - file.set_len( 0 )?; - file.seek( SeekFrom::Start( 0 ) )?; - file.write_all( content.as_bytes() )?; - Ok( () ) + let header = header_param.to_header().map_err( | e | ( report.clone(), e.into() ) )?; + let content : String = TAGS_TEMPLATE.get().unwrap().replace + ( + &content, + &format!( "\n{header}\n" ) + ).into(); + + file.set_len( 0 ).map_err( | e | ( report.clone(), e.into() ) )?; + file.seek( SeekFrom::Start( 0 ) ).map_err( | e | ( report.clone(), e.into() ) )?; + file.write_all( content.as_bytes() ).map_err( | e | ( report.clone(), e.into() ) )?; + report.touched_file = read_me_path; + report.success = true; + Ok( report ) } } @@ -153,4 +243,6 @@ crate::mod_interface! { /// Generate header. orphan use readme_header_renew; + /// Report. + orphan use MainHeaderRenewReport; } \ No newline at end of file diff --git a/module/move/willbe/src/action/readme_modules_headers_renew.rs b/module/move/willbe/src/action/readme_modules_headers_renew.rs index ea07a48c99..5809369b10 100644 --- a/module/move/willbe/src/action/readme_modules_headers_renew.rs +++ b/module/move/willbe/src/action/readme_modules_headers_renew.rs @@ -2,7 +2,7 @@ mod private { use crate::*; use _path::AbsolutePath; - use action::readme_health_table_renew::{ readme_path, Stability, stability_generate }; + use action::readme_health_table_renew::{ readme_path, Stability, stability_generate, find_example_file }; use package::Package; use wtools::error:: { @@ -10,17 +10,23 @@ mod private for_app:: { Result, - Error, + Error as wError, Context, }, }; use std::borrow::Cow; + use std::collections::BTreeSet; + use std::fmt::{Display, Formatter}; use std::fs::{ OpenOptions }; use std::io::{ Read, Seek, SeekFrom, Write }; use std::path::PathBuf; use convert_case::{ Case, Casing }; use regex::Regex; - use crate::action::readme_health_table_renew::find_example_file; + use entity::WorkspaceError; + use manifest::private::CrateDirError; + use package::PackageError; + use error_tools::for_lib::Error; + use error_tools::dependency::*; // aaa : for Petro : rid off crate::x. ask // aaa : add `use crate::*` first @@ -31,6 +37,56 @@ mod private TAGS_TEMPLATE.set( Regex::new( r"(.|\n|\r\n)+" ).unwrap() ).ok(); } + /// Report. + #[ derive( Debug, Default, Clone ) ] + pub struct ModulesHeadersRenewReport + { + found_files : BTreeSet< PathBuf >, + touched_files : BTreeSet< PathBuf >, + } + + impl Display for ModulesHeadersRenewReport + { + fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result + { + if self.touched_files.len() < self.found_files.len() + { + writeln!( f, "Something went wrong.\n{}/{} was touched.", self.found_files.len(), self.touched_files.len() )?; + return Ok(()) + } + writeln!( f, "Touched files :" )?; + let mut count = self.found_files.len(); + for path in &self.touched_files + { + if let Some( file_path ) = path.to_str() + { + writeln!( f, "{file_path}" )?; + count -= 1; + } + } + if count != 0 + { + writeln!( f, "Other {count} files contains non-UTF-8 characters." )?; + } + Ok( () ) + } + } + + #[ derive( Debug, Error ) ] + pub enum ModulesHeadersRenewError + { + #[ error( "Common error: {0}" ) ] + Common(#[ from ] wError ), + #[ error( "I/O error: {0}" ) ] + IO( #[ from ] std::io::Error ), + #[ error( "Workspace error: {0}" ) ] + Workspace( #[ from ] WorkspaceError), + #[ error( "Package error: {0}" ) ] + Package( #[ from ] PackageError), + #[ error( "Directory error: {0}" ) ] + Directory( #[ from ] CrateDirError ), + } + /// The `ModuleHeader` structure represents a set of parameters, used for creating url for header. struct ModuleHeader { @@ -45,13 +101,13 @@ mod private { /// Create `ModuleHeader` instance from the folder where Cargo.toml is stored. - fn from_cargo_toml( package : Package, default_discord_url : &Option< String > ) -> Result< Self > + fn from_cargo_toml( package : Package, default_discord_url : &Option< String > ) -> Result< Self, ModulesHeadersRenewError > { let stability = package.stability()?; let module_name = package.name()?; - let repository_url = package.repository()?.ok_or_else::< Error, _ >( || err!( "Fail to find repository_url in module`s Cargo.toml" ) )?; + let repository_url = package.repository()?.ok_or_else::< wError, _ >( || err!( "Fail to find repository_url in module`s Cargo.toml" ) )?; let discord_url = package.discord_url()?.or_else( || default_discord_url.clone() ); Ok @@ -68,14 +124,14 @@ mod private } /// Convert `ModuleHeader`to header. - fn to_header( self, workspace_path : &str ) -> Result< String > + fn to_header( self, workspace_path : &str ) -> Result< String, ModulesHeadersRenewError > { let discord = self.discord_url.map( | discord_url | format!( " [![discord](https://img.shields.io/discord/872391416519737405?color=eee&logo=discord&logoColor=eee&label=ask)]({discord_url})" ) ) .unwrap_or_default(); - let repo_url = url::extract_repo_url( &self.repository_url ).and_then( | r | url::git_info_extract( &r ).ok() ).ok_or_else::< Error, _ >( || err!( "Fail to parse repository url" ) )?; + let repo_url = url::extract_repo_url( &self.repository_url ).and_then( | r | url::git_info_extract( &r ).ok() ).ok_or_else::< wError, _ >( || err!( "Fail to parse repository url" ) )?; let example = if let Some( name ) = find_example_file( self.module_path.as_path(), &self.module_name ) { // qqq : for Petro : Hardcoded Strings, would be better to use `PathBuf` to avoid separator mismatch on Windows and Unix @@ -124,28 +180,32 @@ mod private /// [![experimental](https://raster.shields.io/static/v1?label=&message=experimental&color=orange)](https://github.com/emersion/stability-badges#experimental) | [![rust-status](https://github.com/Username/test/actions/workflows/ModuleChainOfPackagesAPush.yml/badge.svg)](https://github.com/Username/test/actions/workflows/ModuleChainOfPackagesAPush.yml)[![docs.rs](https://img.shields.io/docsrs/_chain_of_packages_a?color=e3e8f0&logo=docs.rs)](https://docs.rs/_chain_of_packages_a)[![Open in Gitpod](https://raster.shields.io/static/v1?label=try&message=online&color=eee&logo=gitpod&logoColor=eee)](https://gitpod.io/#RUN_PATH=.,SAMPLE_FILE=sample%2Frust%2F_chain_of_packages_a_trivial%2Fsrc%2Fmain.rs,RUN_POSTFIX=--example%20_chain_of_packages_a_trivial/https://github.com/Username/test) /// /// ``` - pub fn readme_modules_headers_renew( path : AbsolutePath ) -> Result< () > + pub fn readme_modules_headers_renew( path : AbsolutePath ) -> Result< ModulesHeadersRenewReport, ( ModulesHeadersRenewReport, ModulesHeadersRenewError ) > { + let mut report = ModulesHeadersRenewReport::default(); regexes_initialize(); - let cargo_metadata = Workspace::with_crate_dir( CrateDir::try_from( path )? )?; - let discord_url = cargo_metadata.discord_url()?; - for path in cargo_metadata.packages()?.into_iter().filter_map( | p | AbsolutePath::try_from( p.manifest_path() ).ok()) + let cargo_metadata = Workspace::with_crate_dir( CrateDir::try_from( path ).map_err( | e | ( report.clone(), e.into() ) )? ).map_err( | e | ( report.clone(), e.into() ) )?; + let discord_url = cargo_metadata.discord_url().map_err( | e | ( report.clone(), e.into() ) )?; + let paths = cargo_metadata.packages().map_err( | e | ( report.clone(), e.into() ) )?.into_iter().filter_map( | p | AbsolutePath::try_from( p.manifest_path() ).ok()).collect::< Vec< _ > >(); + report.found_files = paths.iter().map( | ap | ap.as_ref().to_path_buf() ).collect(); + for path in paths { let read_me_path = path .parent() .unwrap() - .join( readme_path( path.parent().unwrap().as_ref() ).ok_or_else::< Error, _ >( || err!( "Fail to find README.md" ) )? ); + .join( readme_path( path.parent().unwrap().as_ref() ).ok_or_else::< wError, _ >( || err!( "Fail to find README.md" ) ).map_err( | e | ( report.clone(), e.into() ) )? ); - let pakage = Package::try_from( path )?; - let header = ModuleHeader::from_cargo_toml( pakage, &discord_url )?; + let pakage = Package::try_from( path.clone() ).map_err( | e | ( report.clone(), e.into() ) )?; + let header = ModuleHeader::from_cargo_toml( pakage.into(), &discord_url ).map_err( | e | ( report.clone(), e.into() ) )?; let mut file = OpenOptions::new() .read( true ) .write( true ) - .open( &read_me_path )?; + .open( &read_me_path ) + .map_err( | e | ( report.clone(), e.into() ) )?; let mut content = String::new(); - file.read_to_string( &mut content )?; + file.read_to_string( &mut content ).map_err( | e | ( report.clone(), e.into() ) )?; let raw_params = TAGS_TEMPLATE .get() @@ -157,13 +217,14 @@ mod private _ = query::parse( raw_params ).context( "Fail to parse raw params." ); - let content = header_content_generate( &content, header, raw_params, cargo_metadata.workspace_root()?.to_str().unwrap() )?; + let content = header_content_generate( &content, header, raw_params, cargo_metadata.workspace_root().map_err( | e | ( report.clone(), e.into() ) )?.to_str().unwrap() ).map_err( | e | ( report.clone(), e.into() ) )?; - file.set_len( 0 )?; - file.seek( SeekFrom::Start( 0 ) )?; - file.write_all( content.as_bytes() )?; + file.set_len( 0 ).map_err( | e | ( report.clone(), e.into() ) )?; + file.seek( SeekFrom::Start( 0 ) ).map_err( | e | ( report.clone(), e.into() ) )?; + file.write_all( content.as_bytes() ).map_err( | e | ( report.clone(), e.into() ) )?; + report.touched_files.insert( path.as_ref().to_path_buf() ); } - Ok( () ) + Ok( report ) } fn header_content_generate< 'a >( content : &'a str, header : ModuleHeader, raw_params : &str, workspace_root : &str ) -> Result< Cow< 'a, str > > @@ -178,4 +239,6 @@ crate::mod_interface! { /// Generate headers in modules orphan use readme_modules_headers_renew; + /// report + orphan use ModulesHeadersRenewReport; } \ No newline at end of file diff --git a/module/move/willbe/src/command/main_header.rs b/module/move/willbe/src/command/main_header.rs index a9ddd22743..558f2a625b 100644 --- a/module/move/willbe/src/command/main_header.rs +++ b/module/move/willbe/src/command/main_header.rs @@ -3,12 +3,25 @@ mod private use crate::*; use action; use _path::AbsolutePath; - use error_tools::{ for_app::Context, Result }; + use error_tools::Result; + use wtools::error::anyhow::Error; /// Generates header to main Readme.md file. pub fn readme_header_renew() -> Result< () > { - action::readme_header_renew( AbsolutePath::try_from( std::env::current_dir()? )? ).context( "Fail to create table" ) + match action::readme_header_renew( AbsolutePath::try_from( std::env::current_dir()? )? ) + { + Ok( report ) => + { + println!( "{report}" ); + Ok( () ) + } + Err( ( report, e ) ) => + { + eprintln!( "{report}" ); + Err( Error::from( e ).context( "Fail to generate main header." ) ) + } + } } } diff --git a/module/move/willbe/src/command/mod.rs b/module/move/willbe/src/command/mod.rs index 7fc98cf913..74f58a48bc 100644 --- a/module/move/willbe/src/command/mod.rs +++ b/module/move/willbe/src/command/mod.rs @@ -256,6 +256,12 @@ with_gitpod: If set to 1, a column with a link to Gitpod will be added. Clicking .routine( command::readme_modules_headers_renew ) .end() + .command( "readme.headers.renew" ) + .hint( "Aggregation of two command : `readme.header.renew` and `readme.modules.headers.renew`.\n Generated headers in workspace members and in main Readme.md file.") + .long_hint( "Generate header which contains a badge with the general status of workspace, a link to discord, an example in gitpod and documentation in workspace`s Readme.md file.\n For use this command you need to specify:\n\n[workspace.metadata]\nmaster_branch = \"alpha\"\nworkspace_name = \"wtools\"\nrepo_url = \"https://github.com/Wandalen/wTools\"\ndiscord_url = \"https://discord.gg/123123\"\n\nin workspace's Cargo.toml.\n\nGenerates header for each workspace member which contains a badge with the status of crate, a link to discord, an example in gitpod and documentation in crate Readme.md file.\nFor use this command you need to specify:\n\n[package]\nname = \"test_module\"\nrepository = \"https://github.com/Username/ProjectName/tree/master/module/test_module\"\n...\n[package.metadata]\nstability = \"stable\" (Optional)\ndiscord_url = \"https://discord.gg/1234567890\" (Optional)\n\nin module's Cargo.toml.") + .routine( command::readme_headers_renew ) + .end() + .command( "features" ) .hint( "Lists features of the package" ) .long_hint( "Lists features of the package located in a folder.\nWill list either separate package features or features for every package of a workspace") @@ -285,6 +291,8 @@ crate::mod_interface! layer publish; /// Used to compare local and published versions of a specific package. layer publish_diff; + /// Combination of two commands `main_header` and `readme_modules_headers_renew`. + layer readme_headers_renew; /// Generates health table in main Readme.md file of workspace. // aaa : for Petro : what a table?? // aaa : add more details to documentation diff --git a/module/move/willbe/src/command/readme_headers_renew.rs b/module/move/willbe/src/command/readme_headers_renew.rs new file mode 100644 index 0000000000..d8b4edd5c5 --- /dev/null +++ b/module/move/willbe/src/command/readme_headers_renew.rs @@ -0,0 +1,118 @@ +mod private +{ + use crate::*; + use _path::AbsolutePath; + use action; + use wtools::error::anyhow::Error; + use error_tools::{ Result, err }; + use std::fmt::{ Display, Formatter }; + + #[ derive( Debug, Default ) ] + struct ReadmeHeadersRenewReport + { + main_header_renew_report : action::MainHeaderRenewReport, + main_header_renew_error : Option< Error >, + modules_headers_renew_report : action::ModulesHeadersRenewReport, + modules_headers_renew_error : Option< Error >, + } + + impl Display for ReadmeHeadersRenewReport + { + fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result + { + match ( &self.main_header_renew_error, &self.modules_headers_renew_error ) + { + ( Some( main ), Some( modules ) ) => + { + writeln! + ( + f, + "Main header renew report : \n{}\nError : \n{:?}\nModules headers renew report : \n{}\nError : \n{:?}", + self.main_header_renew_report, main, self.modules_headers_renew_report, modules + )?; + } + ( Some( main ), None ) => + { + writeln! + ( + f, + "Main header renew report : \n{}\nError : \n{:?}\nModules headers renew report : \n{}", + self.main_header_renew_report, main, self.modules_headers_renew_report + )?; + } + ( None, Some( modules) ) => + { + writeln! + ( + f, + "Main header renew report : \n{}\nModules headers renew report : \n{}\nError : \n{:?}\n", + self.main_header_renew_report, self.modules_headers_renew_report, modules + )?; + } + ( None, None ) => + { + writeln! + ( + f, + "Main header renew report : \n{}\n\nModules headers renew report : \n{}", + self.main_header_renew_report, self.modules_headers_renew_report + )?; + } + } + Ok( () ) + } + } + + + /// Aggregates two commands: `generate_modules_headers` & `generate_main_header` + pub fn readme_headers_renew() -> Result< () > + { + let mut report = ReadmeHeadersRenewReport::default(); + let absolute_path = AbsolutePath::try_from( std::env::current_dir()? )?; + let mut fail = false; + + match action::readme_header_renew( absolute_path.clone() ) + { + Ok( r ) => + { + report.main_header_renew_report = r; + } + Err( ( r, error ) ) => + { + fail = true; + report.main_header_renew_report = r; + report.main_header_renew_error = Some( Error::from( error ) ); + } + }; + match action::readme_modules_headers_renew( absolute_path ) + { + Ok( r ) => + { + report.modules_headers_renew_report = r; + } + Err( ( r, error ) ) => + { + fail = true; + report.modules_headers_renew_report = r; + report.modules_headers_renew_error = Some( Error::from( error ) ); + } + } + + if fail + { + eprintln!( "{report}" ); + Err( err!( "Something went wrong" ) ) + } + else + { + println!( "{report}" ); + Ok( () ) + } + } +} + +crate::mod_interface! +{ + /// Generate header's. + orphan use readme_headers_renew; +} \ No newline at end of file diff --git a/module/move/willbe/src/command/readme_modules_headers_renew.rs b/module/move/willbe/src/command/readme_modules_headers_renew.rs index af1c8c0805..e959c12365 100644 --- a/module/move/willbe/src/command/readme_modules_headers_renew.rs +++ b/module/move/willbe/src/command/readme_modules_headers_renew.rs @@ -2,12 +2,24 @@ mod private { use crate::*; use _path::AbsolutePath; - use wtools::error::{ for_app::Context, Result }; + use wtools::error::{ for_app::Error, Result }; /// Generate headers for workspace members pub fn readme_modules_headers_renew() -> Result< () > { - action::readme_modules_headers_renew( AbsolutePath::try_from( std::env::current_dir()? )? ).context( "Fail to generate headers" ) + match action::readme_modules_headers_renew( AbsolutePath::try_from( std::env::current_dir()? )? ) + { + Ok( report ) => + { + println!( "{report}" ); + Ok( () ) + } + Err( ( report, e ) ) => + { + eprintln!( "{report}" ); + Err( Error::from( e ).context( "Fail to generate modules headers." ) ) + } + } } } diff --git a/module/move/willbe/src/entity/manifest.rs b/module/move/willbe/src/entity/manifest.rs index fdb04d89bb..d167175177 100644 --- a/module/move/willbe/src/entity/manifest.rs +++ b/module/move/willbe/src/entity/manifest.rs @@ -18,9 +18,11 @@ pub( crate ) mod private }; use _path::AbsolutePath; + /// `CrateDirError` enum represents errors when creating a `CrateDir` object. #[ derive( Debug, Error ) ] pub enum CrateDirError { + /// Indicates a validation error with a descriptive message. #[ error( "Failed to create a `CrateDir` object due to `{0}`" ) ] Validation( String ), } @@ -287,6 +289,7 @@ crate::mod_interface! exposed use Manifest; exposed use CrateDir; orphan use ManifestError; + orphan use CrateDirError; protected use open; protected use repo_url; }