Skip to main content

acorn/analyzer/
mod.rs

1//! # Prose analyzer module
2//!
3//! This is where we keep functions and interfaces necessary to execute ACORN's automated editorial style guide as well as content readability analyzer.
4//!
5//!
6use crate::analyzer::error::Location as ErrorLocation;
7use crate::analyzer::vale::{ValeOutput, ValeOutputItem};
8#[cfg(feature = "std")]
9use crate::io::http::get;
10use crate::io::InputOutput;
11use crate::io::{command_exists, download_binary, extract_zip, file_checksum, make_executable, standard_project_folder, ApiResult};
12use crate::prelude::{self, create_dir_all, remove_file, write, Arc, Command, CommandOutput, File, HashMap, Path, PathBuf, Stdio};
13use crate::schema::pid::raid;
14use crate::schema::pid::{PersistentIdentifier, PersistentIdentifierParse, DOI};
15use crate::schema::research_activity::ResearchActivity;
16use crate::schema::standard::cff::{Cff, Identifier, IdentifierType, Reference};
17use crate::schema::standard::text::{Docx, Text};
18use crate::schema::standard::{datacite, dcat, huwise, invenio};
19use crate::schema::{Organization, ProgrammingLanguage, Website};
20use crate::util::constants::app::APPLICATION;
21use crate::util::constants::vale::{CUSTOM_VALE_PACKAGE_NAME, DEFAULT_VALE_PACKAGE_URL, DEFAULT_VALE_ROOT, VALE_RELEASES_URL, VALE_VERSION};
22use crate::util::{is_uri_or_path, Constant, Label, SemanticVersion, StringConversion};
23use crate::{check, check_err, check_ok};
24use crate::{cmd, skip};
25use crate::{Location, Repository};
26use async_trait::async_trait;
27#[cfg(feature = "analysis")]
28use bat::PrettyPrinter;
29use color_eyre::eyre::eyre;
30use color_eyre::owo_colors::OwoColorize;
31use convert_case::{Case, Casing};
32use flate2::read::GzDecoder;
33use futures::future::{join_all, BoxFuture, FutureExt};
34use ini::Ini;
35use lychee_lib::{CacheStatus, Response, Status};
36#[cfg(feature = "analysis")]
37use polars::datatypes::PlSmallStr;
38#[cfg(feature = "analysis")]
39use polars::prelude::{DataFrame, PolarsResult};
40use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
41use tar::Archive;
42use tracing::{debug, error, info};
43use validator::Validate;
44use validator::ValidationErrorsKind;
45use which::which;
46
47pub mod check;
48pub mod error;
49pub mod readability;
50pub mod vale;
51
52pub use check::{Check, CheckCategory, CheckOptions, CheckSeverity, IntoChecks, Standard};
53
54#[cfg(feature = "analysis")]
55pub use check::{checks_to_csv, checks_to_dataframe, summary, Analysis, IntoRow};
56
57#[cfg(feature = "analysis")]
58use check::{check_prose_for, check_readability_for};
59use error::{process, ErrorKind};
60use vale::{Vale, ValeConfig};
61
62/// Trait for static analyzers (e.g. Vale)
63#[async_trait]
64pub trait StaticAnalyzer<Config: StaticAnalyzerConfig> {
65    /// Get command name (e.g. "vale")
66    fn command(&self) -> String;
67    /// Download binary
68    async fn download(self, config: Option<Config>, skip_verify_checksum: bool) -> Self;
69    /// Download checksum values
70    async fn download_checksums(self) -> ApiResult<HashMap<String, String>>;
71    /// Extract binary
72    fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf;
73    /// Resolve analyzer
74    async fn resolve(_config: Config, _is_offline: bool, _skip_verify_checksum: bool) -> Self;
75    /// Run analyzer on content
76    async fn run(&self, path: PathBuf, content: String, output: Option<String>) -> Vec<Check>;
77    /// Perform sync operation (only applies to Vale)
78    async fn sync(self, is_offline: bool, quiet: bool) -> ApiResult<()>;
79    /// Set binary
80    fn with_binary<P>(self, path: P) -> Self
81    where
82        P: Into<PathBuf>;
83    /// Set config
84    fn with_config(self, value: Config) -> Self;
85    /// Set system command
86    fn with_system_command(self) -> Self;
87    /// Set version
88    fn with_version(self, value: String) -> Self;
89}
90/// Trait for static analyzer configuration (e.g. .vale.ini)
91#[async_trait]
92pub trait StaticAnalyzerConfig {
93    /// Convert to INI
94    async fn ini(self) -> Ini;
95    /// Save configuration
96    async fn save(self) -> Self;
97    /// Set parent path of configuration
98    fn with_path(self, path: PathBuf) -> Self;
99    /// Resolve package source: convert bare package name to download URL or pass through existing URL/path
100    fn resolve_package(value: impl AsRef<str>) -> String;
101}
102#[cfg(feature = "analysis")]
103#[async_trait]
104impl Analysis for Cff {
105    fn standard() -> Standard {
106        Standard::CitationFileFormat
107    }
108    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
109        check_prose_for::<Self>(paths, options).await
110    }
111    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
112        paths
113            .par_iter()
114            .map(|path| match Self::read(path) {
115                | Ok(_) => check_ok!(CheckCategory::Quality),
116                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
117            })
118            .collect()
119    }
120    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
121        check_readability_for::<Self>(paths, options)
122    }
123    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
124        paths
125            .par_iter()
126            .flat_map(|path| match Self::read(path) {
127                | Ok(data) => {
128                    let uri = Some(path.file_name_with_parent());
129                    let data = Arc::new(data);
130                    collect_validation_checks(data.as_ref())
131                        .into_iter()
132                        .map(|issue| issue.with_uri(uri.clone()))
133                        .collect::<Vec<_>>()
134                }
135                | Err(why) => why
136                    .to_string()
137                    .lines()
138                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
139                    .collect(),
140            })
141            .collect()
142    }
143    #[cfg(feature = "std")]
144    async fn check_websites(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
145        let futures = paths.iter().map(|path| {
146            let path = path.clone();
147            async move {
148                match Self::read(&path) {
149                    | Ok(data) => {
150                        let _data = Arc::new(data);
151                        let Cff {
152                            identifiers,
153                            license_url,
154                            references,
155                            repository,
156                            repository_artifact,
157                            repository_code,
158                            url,
159                            ..
160                        } = _data.as_ref();
161                        let root_futures = vec![
162                            url.as_deref().map(|u| link_check(Some(u), Some("url".into()))),
163                            license_url.as_deref().map(|u| link_check(Some(u), Some("license_url".into()))),
164                            repository.as_deref().map(|u| link_check(Some(u), Some("repository".into()))),
165                            repository_artifact
166                                .as_deref()
167                                .map(|u| link_check(Some(u), Some("repository_artifact".into()))),
168                            repository_code.as_deref().map(|u| link_check(Some(u), Some("repository_code".into()))),
169                        ]
170                        .into_iter()
171                        .flatten()
172                        .collect::<Vec<_>>();
173                        let identifier_futures = identifiers
174                            .as_ref()
175                            .into_iter()
176                            .flatten()
177                            .enumerate()
178                            .filter_map(|(i, Identifier { kind, value, .. })| match kind {
179                                | IdentifierType::Doi => {
180                                    let maybe_doi = DOI::from_string(value).url();
181                                    let url = if maybe_doi.is_empty() { value.to_owned() } else { maybe_doi };
182                                    Some(link_check(Some(url), Some(format!("identifiers[{i}].value"))))
183                                }
184                                | IdentifierType::Url => Some(link_check(Some(value.as_str()), Some(format!("identifiers[{i}].value")))),
185                                | _ => None,
186                            })
187                            .collect::<Vec<_>>();
188                        let reference_futures = references
189                            .as_ref()
190                            .into_iter()
191                            .flatten()
192                            .enumerate()
193                            .flat_map(
194                                |(
195                                    i,
196                                    Reference {
197                                        doi,
198                                        collection_doi,
199                                        license_url,
200                                        repository,
201                                        repository_artifact,
202                                        repository_code,
203                                        url,
204                                        ..
205                                    },
206                                )| {
207                                    vec![
208                                        doi.as_deref().map(|d| {
209                                            let url = DOI::from_string(d).url();
210                                            link_check(Some(url), Some(format!("references[{i}].doi")))
211                                        }),
212                                        collection_doi.as_deref().map(|d| {
213                                            let url = DOI::from_string(d).url();
214                                            link_check(Some(url), Some(format!("references[{i}].collection_doi")))
215                                        }),
216                                        license_url
217                                            .as_deref()
218                                            .map(|u| link_check(Some(u), Some(format!("references[{i}].license_url")))),
219                                        repository
220                                            .as_deref()
221                                            .map(|u| link_check(Some(u), Some(format!("references[{i}].repository")))),
222                                        repository_artifact
223                                            .as_deref()
224                                            .map(|u| link_check(Some(u), Some(format!("references[{i}].repository_artifact")))),
225                                        repository_code
226                                            .as_deref()
227                                            .map(|u| link_check(Some(u), Some(format!("references[{i}].repository_code")))),
228                                        url.as_deref().map(|u| link_check(Some(u), Some(format!("references[{i}].url")))),
229                                    ]
230                                    .into_iter()
231                                    .flatten()
232                                },
233                            )
234                            .collect::<Vec<_>>();
235                        let futures: Vec<_> = root_futures.into_iter().chain(identifier_futures).chain(reference_futures).collect();
236                        join_all(futures)
237                            .await
238                            .into_iter()
239                            .map(|check| check.with_uri(Some(path.file_name_with_parent())))
240                            .collect()
241                    }
242                    | Err(why) => vec![check_err!(CheckCategory::Link, context: why.to_string()).with_uri(Some(path.file_name_with_parent()))],
243                }
244            }
245        });
246        join_all(futures).await.into_iter().flatten().collect()
247    }
248    fn output_path(path: &Path, _data: &Self) -> PathBuf {
249        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
250    }
251}
252#[cfg(feature = "analysis")]
253#[async_trait]
254impl Analysis for datacite::Record {
255    fn standard() -> Standard {
256        Standard::Datacite
257    }
258    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
259        check_prose_for::<Self>(paths, options).await
260    }
261    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
262        paths
263            .par_iter()
264            .map(|path| match Self::read(path) {
265                | Ok(_) => check_ok!(CheckCategory::Quality),
266                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
267            })
268            .collect()
269    }
270    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
271        check_readability_for::<Self>(paths, options)
272    }
273    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
274        paths
275            .par_iter()
276            .flat_map(|path| match Self::read(path) {
277                | Ok(data) => {
278                    let uri = Some(path.file_name_with_parent());
279                    let data = Arc::new(data);
280                    collect_validation_checks(data.as_ref())
281                        .into_iter()
282                        .map(|issue| issue.with_uri(uri.clone()))
283                        .collect::<Vec<_>>()
284                }
285                | Err(why) => why
286                    .to_string()
287                    .lines()
288                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
289                    .collect(),
290            })
291            .collect()
292    }
293    #[cfg(feature = "std")]
294    async fn check_websites(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
295        let futures = paths.iter().map(|path| {
296            let path = path.clone();
297            async move {
298                match Self::read(&path) {
299                    | Ok(data) => {
300                        let data = Arc::new(data);
301                        let attributes = &data.attributes;
302                        let root_futures = attributes
303                            .url
304                            .as_deref()
305                            .map(|value| link_check(Some(value), Some("attributes.url".into())))
306                            .into_iter();
307                        let rights_futures = attributes.rights_list.iter().flatten().enumerate().filter_map(|(index, value)| {
308                            value
309                                .rights_uri
310                                .as_deref()
311                                .map(|uri| link_check(Some(uri), Some(format!("attributes.rights_list[{index}].rights_uri"))))
312                        });
313                        let related_futures = attributes.related_identifiers.iter().flatten().enumerate().filter_map(|(index, value)| {
314                            match value.related_identifier_type {
315                                | Some(datacite::RelatedIdentifierType::Doi) => {
316                                    let url = DOI::from_string(value.related_identifier.as_str()).url();
317                                    let target = if url.is_empty() { value.related_identifier.clone() } else { url };
318                                    Some(link_check(
319                                        Some(target),
320                                        Some(format!("attributes.related_identifiers[{index}].related_identifier")),
321                                    ))
322                                }
323                                | Some(datacite::RelatedIdentifierType::Url) => Some(link_check(
324                                    Some(value.related_identifier.as_str()),
325                                    Some(format!("attributes.related_identifiers[{index}].related_identifier")),
326                                )),
327                                | _ => None,
328                            }
329                        });
330                        let checks = join_all(root_futures.chain(rights_futures).chain(related_futures))
331                            .await
332                            .into_iter()
333                            .map(|check| check.with_uri(Some(path.file_name_with_parent())))
334                            .collect::<Vec<_>>();
335                        checks
336                    }
337                    | Err(why) => vec![check_err!(CheckCategory::Link, context: why.to_string()).with_uri(Some(path.file_name_with_parent()))],
338                }
339            }
340        });
341        join_all(futures).await.into_iter().flatten().collect()
342    }
343    fn output_path(path: &Path, _data: &Self) -> PathBuf {
344        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
345    }
346}
347#[cfg(feature = "analysis")]
348#[async_trait]
349impl Analysis for dcat::Dataset {
350    fn standard() -> Standard {
351        Standard::Dcat
352    }
353    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
354        check_prose_for::<Self>(paths, options).await
355    }
356    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
357        paths
358            .par_iter()
359            .map(|path| match Self::read(path) {
360                | Ok(_) => check_ok!(CheckCategory::Quality),
361                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
362            })
363            .collect()
364    }
365    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
366        check_readability_for::<Self>(paths, options)
367    }
368    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
369        paths
370            .par_iter()
371            .flat_map(|path| match Self::read(path) {
372                | Ok(data) => {
373                    let uri = Some(path.file_name_with_parent());
374                    let data = Arc::new(data);
375                    collect_validation_checks(data.as_ref())
376                        .into_iter()
377                        .map(|issue| issue.with_uri(uri.clone()))
378                        .collect::<Vec<_>>()
379                }
380                | Err(why) => why
381                    .to_string()
382                    .lines()
383                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
384                    .collect(),
385            })
386            .collect()
387    }
388    #[cfg(feature = "std")]
389    async fn check_websites(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
390        let futures = paths.iter().map(|path| {
391            let path = path.clone();
392            async move {
393                match Self::read(&path) {
394                    | Ok(data) => {
395                        let data = Arc::new(data);
396                        let root_futures = data
397                            .landing_page
398                            .iter()
399                            .flatten()
400                            .enumerate()
401                            .map(|(index, value)| link_check(value.url(), Some(format!("landing_page[{index}]"))));
402                        let distribution_futures = data
403                            .distribution
404                            .iter()
405                            .flatten()
406                            .enumerate()
407                            .flat_map(|(distribution_index, distribution)| {
408                                distribution
409                                    .access_url
410                                    .iter()
411                                    .enumerate()
412                                    .map(move |(index, value)| {
413                                        link_check(
414                                            Some(value.as_str()),
415                                            Some(format!("distribution[{distribution_index}].access_url[{index}]")),
416                                        )
417                                    })
418                                    .chain(distribution.download_url.iter().flatten().enumerate().map(move |(index, value)| {
419                                        link_check(
420                                            Some(value.as_str()),
421                                            Some(format!("distribution[{distribution_index}].download_url[{index}]")),
422                                        )
423                                    }))
424                            });
425                        let checks = join_all(root_futures.chain(distribution_futures))
426                            .await
427                            .into_iter()
428                            .map(|check| check.with_uri(Some(path.file_name_with_parent())))
429                            .collect::<Vec<_>>();
430                        checks
431                    }
432                    | Err(why) => vec![check_err!(CheckCategory::Link, context: why.to_string()).with_uri(Some(path.file_name_with_parent()))],
433                }
434            }
435        });
436        join_all(futures).await.into_iter().flatten().collect()
437    }
438    fn output_path(path: &Path, _data: &Self) -> PathBuf {
439        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
440    }
441}
442#[cfg(feature = "analysis")]
443#[async_trait]
444impl Analysis for invenio::Record {
445    fn standard() -> Standard {
446        Standard::Invenio
447    }
448    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
449        check_prose_for::<Self>(paths, options).await
450    }
451    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
452        paths
453            .par_iter()
454            .map(|path| match Self::read(path) {
455                | Ok(_) => check_ok!(CheckCategory::Quality),
456                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
457            })
458            .collect()
459    }
460    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
461        check_readability_for::<Self>(paths, options)
462    }
463    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
464        paths
465            .par_iter()
466            .flat_map(|path| match Self::read(path) {
467                | Ok(data) => {
468                    let uri = Some(path.file_name_with_parent());
469                    let data = Arc::new(data);
470                    collect_validation_checks(data.as_ref())
471                        .into_iter()
472                        .map(|issue| issue.with_uri(uri.clone()))
473                        .collect::<Vec<_>>()
474                }
475                | Err(why) => why
476                    .to_string()
477                    .lines()
478                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
479                    .collect(),
480            })
481            .collect()
482    }
483    #[cfg(feature = "std")]
484    async fn check_websites(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
485        let futures = paths.iter().map(|path| {
486            let path = path.clone();
487            async move {
488                match Self::read(&path) {
489                    | Ok(data) => {
490                        let data = Arc::new(data);
491                        let pid_futures = data.pids.iter().flat_map(|pids| {
492                            [
493                                pids.doi.as_ref().map(|value| {
494                                    link_check(
495                                        Some(DOI::from_string(value.identifier.as_str()).url()),
496                                        Some("pids.doi.identifier".into()),
497                                    )
498                                }),
499                                pids.concept_doi.as_ref().map(|value| {
500                                    link_check(
501                                        Some(DOI::from_string(value.identifier.as_str()).url()),
502                                        Some("pids.concept_doi.identifier".into()),
503                                    )
504                                }),
505                            ]
506                            .into_iter()
507                            .flatten()
508                        });
509                        let related_futures = data.metadata.iter().flat_map(|metadata| {
510                            metadata.related_identifiers.iter().flatten().enumerate().filter_map(|(index, value)| {
511                                match value.scheme.to_lowercase().as_str() {
512                                    | "doi" => {
513                                        let url = DOI::from_string(value.identifier.as_str()).url();
514                                        let target = if url.is_empty() { value.identifier.clone() } else { url };
515                                        Some(link_check(
516                                            Some(target),
517                                            Some(format!("metadata.related_identifiers[{index}].identifier")),
518                                        ))
519                                    }
520                                    | "url" => Some(link_check(
521                                        Some(value.identifier.as_str()),
522                                        Some(format!("metadata.related_identifiers[{index}].identifier")),
523                                    )),
524                                    | _ => None,
525                                }
526                            })
527                        });
528                        let rights_futures = data.metadata.iter().flat_map(|metadata| {
529                            metadata.rights.iter().flatten().enumerate().filter_map(|(index, value)| {
530                                value
531                                    .link
532                                    .as_deref()
533                                    .map(|uri| link_check(Some(uri), Some(format!("metadata.rights[{index}].link"))))
534                            })
535                        });
536                        let checks = join_all(pid_futures.chain(related_futures).chain(rights_futures))
537                            .await
538                            .into_iter()
539                            .map(|check| check.with_uri(Some(path.file_name_with_parent())))
540                            .collect::<Vec<_>>();
541                        checks
542                    }
543                    | Err(why) => vec![check_err!(CheckCategory::Link, context: why.to_string()).with_uri(Some(path.file_name_with_parent()))],
544                }
545            }
546        });
547        join_all(futures).await.into_iter().flatten().collect()
548    }
549    fn output_path(path: &Path, _data: &Self) -> PathBuf {
550        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
551    }
552}
553#[cfg(feature = "analysis")]
554#[async_trait]
555impl Analysis for huwise::Dataset {
556    fn standard() -> Standard {
557        Standard::Huwise
558    }
559    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
560        check_prose_for::<Self>(paths, options).await
561    }
562    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
563        paths
564            .par_iter()
565            .map(|path| match Self::read(path) {
566                | Ok(_) => check_ok!(CheckCategory::Quality),
567                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
568            })
569            .collect()
570    }
571    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
572        check_readability_for::<Self>(paths, options)
573    }
574    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
575        paths
576            .par_iter()
577            .flat_map(|path| match Self::read(path) {
578                | Ok(data) => {
579                    let uri = Some(path.file_name_with_parent());
580                    let data = Arc::new(data);
581                    collect_validation_checks(data.as_ref())
582                        .into_iter()
583                        .map(|issue| issue.with_uri(uri.clone()))
584                        .collect::<Vec<_>>()
585                }
586                | Err(why) => why
587                    .to_string()
588                    .lines()
589                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
590                    .collect(),
591            })
592            .collect()
593    }
594    #[cfg(feature = "std")]
595    async fn check_websites(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
596        vec![]
597    }
598    fn output_path(path: &Path, _data: &Self) -> PathBuf {
599        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
600    }
601}
602#[cfg(feature = "analysis")]
603#[async_trait]
604impl Analysis for Docx {
605    fn standard() -> Standard {
606        Standard::Docx
607    }
608    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
609        check_prose_for::<Self>(paths, options).await
610    }
611    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
612        paths
613            .par_iter()
614            .map(|path| match Self::read(path) {
615                | Ok(_) => check_ok!(CheckCategory::Quality),
616                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
617            })
618            .collect()
619    }
620    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
621        check_readability_for::<Self>(paths, options)
622    }
623    async fn check_schema(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
624        vec![]
625    }
626    #[cfg(feature = "std")]
627    async fn check_websites(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
628        vec![]
629    }
630    fn output_path(path: &Path, _data: &Self) -> PathBuf {
631        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
632    }
633}
634#[cfg(feature = "analysis")]
635#[async_trait]
636impl Analysis for ResearchActivity {
637    fn standard() -> Standard {
638        Standard::ResearchActivityData
639    }
640    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
641        check_prose_for::<Self>(paths, options).await
642    }
643    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
644        paths
645            .par_iter()
646            .map(|path| match Self::read(path) {
647                | Ok(_) => check_ok!(CheckCategory::Quality),
648                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
649            })
650            .collect()
651    }
652    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
653        check_readability_for::<Self>(paths, options)
654    }
655    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
656        paths
657            .par_iter()
658            .flat_map(|path| match Self::read(path) {
659                | Ok(data) => {
660                    let uri = Some(path.file_name_with_parent());
661                    let data = Arc::new(data);
662                    collect_validation_checks(data.as_ref())
663                        .into_iter()
664                        .map(|issue| issue.with_uri(uri.clone()))
665                        .collect::<Vec<_>>()
666                }
667                | Err(why) => why
668                    .to_string()
669                    .lines()
670                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
671                    .collect(),
672            })
673            .collect()
674    }
675    #[cfg(feature = "std")]
676    async fn check_websites(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
677        let futures = paths.iter().map(|path| {
678            let path = path.clone();
679            async move {
680                match Self::read(&path) {
681                    | Ok(data) => {
682                        let data = Arc::new(data);
683                        let ResearchActivity { meta, contact, .. } = data.as_ref();
684                        let dois = match &meta.doi {
685                            | Some(values) => values
686                                .iter()
687                                .enumerate()
688                                .map(|(i, doi)| {
689                                    let url = format!("https://doi.org/{doi}");
690                                    link_check(Some(url), Some(format!("meta.doi[{i}]")))
691                                })
692                                .collect::<Vec<_>>(),
693                            | None => vec![],
694                        };
695                        let websites = match &meta.websites {
696                            | Some(values) => values
697                                .iter()
698                                .enumerate()
699                                .map(|(i, Website { url, .. })| link_check(Some(url.as_str()), Some(format!("meta.websites[{i}].url"))))
700                                .collect::<Vec<_>>(),
701                            | None => vec![],
702                        };
703                        let contact = [link_check(Some(contact.url.as_str()), Some("contact.url".into()))];
704                        let links: Vec<_> = [].into_iter().chain(dois).chain(websites).chain(contact).collect();
705                        join_all(links)
706                            .await
707                            .into_iter()
708                            .map(|check| check.with_uri(Some(path.file_name_with_parent())))
709                            .collect()
710                    }
711                    | Err(why) => vec![check_err!(CheckCategory::Link, context: why.to_string()).with_uri(Some(path.file_name_with_parent()))],
712                }
713            }
714        });
715        join_all(futures).await.into_iter().flatten().collect()
716    }
717    fn output_path(path: &Path, data: &Self) -> PathBuf {
718        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("index.json");
719        standard_project_folder("check", None)
720            .join(data.meta.identifier.to_lowercase())
721            .join(filename)
722    }
723}
724#[cfg(feature = "analysis")]
725#[async_trait]
726impl Analysis for Text {
727    fn standard() -> Standard {
728        Standard::Text
729    }
730    async fn check_prose(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
731        check_prose_for::<Self>(paths, options).await
732    }
733    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
734        paths
735            .par_iter()
736            .map(|path| match Self::read(path) {
737                | Ok(_) => check_ok!(CheckCategory::Quality),
738                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
739            })
740            .collect()
741    }
742    async fn check_readability(paths: &[PathBuf], options: Option<&CheckOptions>) -> Vec<Check> {
743        check_readability_for::<Self>(paths, options)
744    }
745    async fn check_schema(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
746        vec![]
747    }
748    #[cfg(feature = "std")]
749    async fn check_websites(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
750        vec![]
751    }
752    fn output_path(path: &Path, _data: &Self) -> PathBuf {
753        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
754    }
755}
756#[cfg(feature = "analysis")]
757#[async_trait]
758impl Analysis for raid::Metadata {
759    fn standard() -> Standard {
760        Standard::Raid
761    }
762    async fn check_prose(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
763        vec![]
764    }
765    async fn check_quality(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
766        paths
767            .par_iter()
768            .map(|path| match Self::read(path.to_path_buf()) {
769                | Ok(_) => check_ok!(CheckCategory::Quality),
770                | Err(why) => check_err!(CheckCategory::Quality, context: why.to_string()),
771            })
772            .collect()
773    }
774    async fn check_readability(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
775        vec![]
776    }
777    async fn check_schema(paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
778        paths
779            .par_iter()
780            .flat_map(|path| match Self::read(path.to_path_buf()) {
781                | Ok(data) => {
782                    let uri = Some(path.file_name_with_parent());
783                    collect_validation_checks(&data)
784                        .into_iter()
785                        .map(|issue| issue.with_uri(uri.clone()))
786                        .collect::<Vec<_>>()
787                }
788                | Err(why) => why
789                    .to_string()
790                    .lines()
791                    .map(|line| check_err!(CheckCategory::Schema, context: line.to_string()).with_uri(Some(path.file_name_with_parent())))
792                    .collect(),
793            })
794            .collect()
795    }
796    #[cfg(feature = "std")]
797    async fn check_websites(_paths: &[PathBuf], _options: Option<&CheckOptions>) -> Vec<Check> {
798        vec![]
799    }
800    fn output_path(path: &Path, _data: &Self) -> PathBuf {
801        standard_project_folder("check", None).join(path.to_path_buf().file_name_with_parent())
802    }
803}
804#[async_trait]
805impl StaticAnalyzer<ValeConfig> for Vale {
806    fn command(&self) -> String {
807        "vale".to_string()
808    }
809    /// Resolve Vale
810    /// ### Notes
811    /// - Will use system `vale` command if available
812    /// - Will use local `vale` binary if available (will expect local binary if offline)
813    /// - Will download `vale` binary if not available by other means
814    async fn resolve(config: ValeConfig, is_offline: bool, skip_verify_checksum: bool) -> Vale {
815        fn any_exist<S>(paths: Vec<S>) -> bool
816        where
817            S: Into<PathBuf>,
818        {
819            paths.into_iter().any(|s| s.into().exists())
820        }
821        let root = DEFAULT_VALE_ROOT;
822        let name = "vale";
823        let init = Vale::init().build();
824        let vale = if command_exists(name) {
825            let init_with_config = init.with_config(config);
826            init_with_config.with_system_command()
827        } else if is_offline || any_exist(vec![format!("{root}{name}"), format!("{root}{name}.exe")]) {
828            info!("=> {} Local {} binary", Label::using(), name.green().bold());
829            #[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
830            {
831                init.with_config(config).with_binary(format!("{root}{name}"))
832            }
833            #[cfg(windows)]
834            {
835                init.with_config(config).with_binary(format!("{root}{name}.exe"))
836            }
837        } else {
838            init.download(Some(config), skip_verify_checksum).await
839        };
840        vale
841    }
842    async fn run(&self, path: PathBuf, content: String, output_format: Option<String>) -> Vec<Check> {
843        let uri = path.file_name_with_parent();
844        let root = path
845            .parent()
846            .map(|value| value.to_path_buf())
847            .unwrap_or_else(|| standard_project_folder("check", None));
848        match create_dir_all(root.clone()) {
849            | Ok(_) => {}
850            | Err(why) => error!(path = root.clone().to_absolute_string(), "=> {} Create — {why}", Label::fail()),
851        }
852        match write(&path, content.as_bytes()) {
853            | Ok(_) => {}
854            | Err(why) => {
855                error!(path = path.to_absolute_string(), "=> {} Write file — {why}", Label::fail());
856                return vec![check_err!(
857                    CheckCategory::Prose,
858                    uri: uri,
859                    message: format!("Cannot write analyzer input at {}", path.display())
860                )];
861            }
862        }
863        let binary = match &self.binary {
864            | Some(value) => value,
865            | None => {
866                error!("=> {} {} binary", Label::not_found(), self.command());
867                &PathBuf::from("./.vale/vale")
868            }
869        };
870        match &self.config {
871            | Some(config) => {
872                let binary_path = binary.to_absolute_string();
873                let config_path = config.clone().path.to_absolute_string();
874                let path_string = path.clone().to_absolute_string();
875                let args = [
876                    "--no-exit".to_string(),
877                    "--no-wrap".to_string(),
878                    "--ext".to_string(),
879                    ".md".to_string(),
880                    "--config".to_string(),
881                    config_path,
882                    path_string,
883                ];
884                let result = match output_format {
885                    | Some(value) => {
886                        let args = args.into_iter().chain(["--output".to_string(), value]).collect::<Vec<String>>();
887                        cmd!(binary_path, args)
888                    }
889                    | None => cmd!(binary_path, args),
890                };
891                match result {
892                    | Ok(output) if output.status.success() => {
893                        let parsed = ValeOutput::parse(&output.stdout(), path.clone());
894                        if parsed.is_empty() {
895                            info!("=> {} {} has {}", Label::pass(), uri.underline(), "no prose issues".green());
896                            vec![check_ok!(CheckCategory::Prose, uri: uri)]
897                        } else {
898                            parsed
899                                .into_iter()
900                                .map(|item| {
901                                    let ValeOutputItem { message, severity, .. } = item.clone();
902                                    check!(
903                                        CheckCategory::Prose,
904                                        false,
905                                        severity: severity.into(),
906                                        uri: uri.clone(),
907                                        locator: item.locator(),
908                                        message: message,
909                                        errors: ErrorKind::Vale(vec![item.clone()]),
910                                        data: content.clone(),
911                                    )
912                                })
913                                .collect()
914                        }
915                    }
916                    | Ok(output) => {
917                        let why = output.stderr();
918                        let message = if why.is_empty() {
919                            format!("process exited with status {}", output.status)
920                        } else {
921                            why
922                        };
923                        error!("=> {} Analyze with OK output — {message}", Label::fail());
924                        vec![check_err!(CheckCategory::Prose, message: message)]
925                    }
926                    | Err(why) => {
927                        error!("=> {} Analyze — {why}", Label::fail());
928                        vec![check_err!(CheckCategory::Prose, uri: uri)]
929                    }
930                }
931            }
932            | None => {
933                let title = self.command().to_case(Case::Title);
934                error!("=> {} {} configuration", Label::not_found(), title);
935                vec![check_err!(CheckCategory::Prose, message: uri)]
936            }
937        }
938    }
939    async fn download(self, config: Option<ValeConfig>, skip_verify_checksum: bool) -> Vale {
940        let platform = prelude::vale_release_filename();
941        let release = match self.version {
942            | Some(value) => value,
943            | None => SemanticVersion::from(VALE_VERSION),
944        };
945        let url = format!("{VALE_RELEASES_URL}/download/v{release}/{}_{release}_{platform}", self.command());
946        info!(url, "=> {} Vale release v{release}", Label::using());
947        let binary = match download_binary(&url, ".").await {
948            | Ok(path) => {
949                if !skip_verify_checksum {
950                    let dowloaded_checksum = match self.clone().download_checksums().await {
951                        | Ok(value) => value.get(&platform).unwrap_or(&String::new()).to_string(),
952                        | Err(_) => "".to_string(),
953                    };
954                    if let Some(calculated) = file_checksum(path.clone(), None) {
955                        if !dowloaded_checksum.eq(&calculated.checksum_value) {
956                            error!(dowloaded_checksum, calculated = calculated.checksum_value, "=> {}", Label::invalid());
957                            let _cleanup = remove_file(path.clone());
958                        } else {
959                            info!(checksum = dowloaded_checksum, "=> {} Checksum verification", Label::pass());
960                        }
961                    };
962                } else {
963                    skip!("Checksum verification");
964                }
965                let destination = match config.clone() {
966                    | Some(value) => value.path.parent().map(Path::to_path_buf).unwrap_or(PathBuf::from("./.vale/")),
967                    | None => PathBuf::from("./.vale/"),
968                };
969                let binary = self.clone().extract(path.clone(), Some(destination));
970                if make_executable(&binary) {
971                    let _cleanup = remove_file(path);
972                    Some(binary)
973                } else {
974                    error!("=> {} {} not executable", Label::fail(), self.command());
975                    None
976                }
977            }
978            | Err(why) => {
979                error!(url, "=> {} {} download — {why}", Label::fail(), self.command());
980                None
981            }
982        };
983        let builder = Vale::init().version(release).maybe_binary(binary);
984        builder.config(config.unwrap_or_default()).build()
985    }
986    async fn download_checksums(self) -> ApiResult<HashMap<String, String>> {
987        let release = match self.version {
988            | Some(value) => value,
989            | None => SemanticVersion::from(VALE_VERSION),
990        };
991        let url = format!("{VALE_RELEASES_URL}/download/v{release}/{}_{release}_checksums.txt", self.command());
992        let checksums = match get(url).send().await {
993            | Ok(response) => match response.text().await {
994                | Ok(content) => Ok(content.lines().clone().fold(HashMap::new(), |mut acc: HashMap<String, String>, line| {
995                    let mut values: Vec<&str> = line.split("  ").collect();
996                    if let (Some(raw_key), Some(raw_value)) = (values.pop(), values.pop()) {
997                        let key = raw_key["vale_#.#.#_".len()..].to_string();
998                        acc.insert(key, raw_value.to_string());
999                    }
1000                    acc
1001                })),
1002                | Err(why) => Err(eyre!("Failed to read checksums response — {why}")),
1003            },
1004            | Err(why) => Err(eyre!("Failed to download checksums — {why}")),
1005        };
1006        match checksums {
1007            | Ok(checksums) => {
1008                debug!(
1009                    "=> {} {} checksums {:#?}",
1010                    Label::using(),
1011                    self.command().to_case(Case::Title),
1012                    checksums.dimmed().cyan()
1013                );
1014                Ok(checksums)
1015            }
1016            | Err(why) => Err(why),
1017        }
1018    }
1019    fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf {
1020        let command = self.command();
1021        let parent = match destination {
1022            | Some(value) => value.to_absolute_string(),
1023            | None => format!("./.{command}/"),
1024        };
1025        let extension = path.extension().unwrap_or_default().to_str().unwrap_or_default().to_string();
1026        match extension.as_str() {
1027            | "zip" => match extract_zip(path, Some(parent.into())) {
1028                | Ok(value) => {
1029                    let path = value.join(command);
1030                    if cfg!(windows) {
1031                        path.with_extension("exe")
1032                    } else {
1033                        path
1034                    }
1035                }
1036                | Err(why) => {
1037                    error!("=> {} {command} extract — {why}", Label::fail());
1038                    let path = PathBuf::from(DEFAULT_VALE_ROOT).join(command);
1039                    if cfg!(windows) {
1040                        path.with_extension("exe")
1041                    } else {
1042                        path
1043                    }
1044                }
1045            },
1046            | "gz" => match File::open(path) {
1047                | Ok(tar_gz) => {
1048                    let tar = GzDecoder::new(tar_gz);
1049                    let mut archive = Archive::new(tar);
1050                    match archive.unpack(parent.clone()) {
1051                        | Ok(_) => {
1052                            debug!(parent, "=> {} Extracted {command} binary", Label::using());
1053                            PathBuf::from(format!("{parent}/{command}"))
1054                        }
1055                        | Err(why) => {
1056                            error!("=> {} {command} extract — {why}", Label::fail());
1057                            let path = PathBuf::from(DEFAULT_VALE_ROOT).join(command);
1058                            if cfg!(windows) {
1059                                path.with_extension("exe")
1060                            } else {
1061                                path
1062                            }
1063                        }
1064                    }
1065                }
1066                | Err(why) => {
1067                    error!("=> {} {command} extract — {why}", Label::fail());
1068                    let path = PathBuf::from(DEFAULT_VALE_ROOT).join(command);
1069                    if cfg!(windows) {
1070                        path.with_extension("exe")
1071                    } else {
1072                        path
1073                    }
1074                }
1075            },
1076            | _ => {
1077                error!("=> {} {command} extract — Unsupported format", Label::fail());
1078                PathBuf::from(DEFAULT_VALE_ROOT).join(command)
1079            }
1080        }
1081    }
1082    async fn sync(self, is_offline: bool, quiet: bool) -> ApiResult<()> {
1083        let command = self.command();
1084        let binary_path = match self.binary {
1085            | Some(value) => value,
1086            | None => {
1087                error!("=> {} {} binary", Label::not_found(), command);
1088                PathBuf::from(DEFAULT_VALE_ROOT).join(command)
1089            }
1090        };
1091        let config_path = self.config.unwrap_or_default().path;
1092        let result: ApiResult<()> = if is_offline {
1093            skip!("Vale sync");
1094            Ok(())
1095        } else {
1096            let pipe = || if quiet { Stdio::null() } else { Stdio::inherit() };
1097            let status = Command::new(binary_path.clone())
1098                .arg("--config")
1099                .arg(config_path.clone())
1100                .arg("sync")
1101                .stdout(pipe())
1102                .stderr(pipe())
1103                .status();
1104            match status {
1105                | Ok(value) if value.success() => Ok(()),
1106                | Ok(value) => Err(eyre!("Vale sync failed ({value})")),
1107                | Err(why) => Err(eyre!("Vale sync failed — {why}")),
1108            }
1109        };
1110        match result {
1111            | Ok(_) => {
1112                let parent_dir = config_path.parent().map(|p| p.display().to_string()).unwrap_or_default();
1113                let parent = format!("{parent_dir}/styles/config/vocabularies/{APPLICATION}");
1114                debug!(parent, "=> {} Vocabularies", Label::using());
1115                match create_dir_all(parent.clone()) {
1116                    | Ok(_) => {}
1117                    | Err(why) => error!(directory = parent, "=> {} Create - {why}", Label::fail()),
1118                }
1119                let acronyms = Constant::last_values("acronyms");
1120                let partners = Constant::nth_values("partners", 1);
1121                let sponsors = Constant::nth_values("sponsors", 1);
1122                let abbreviations = Organization::alternative_names();
1123                let words = Constant::read_lines("accept.txt");
1124                let accept_content = acronyms
1125                    .chain(partners)
1126                    .chain(sponsors)
1127                    .chain(abbreviations)
1128                    .chain(words)
1129                    .collect::<Vec<String>>()
1130                    .join("\n");
1131                match write(format!("{parent}/accept.txt"), accept_content.as_bytes()) {
1132                    | Ok(_) => {
1133                        let reject_content = Constant::read_lines("reject.txt").join("\n");
1134                        match write(format!("{parent}/reject.txt"), reject_content.as_bytes()) {
1135                            | Ok(_) => Ok(()),
1136                            | Err(why) => Err(eyre!("Write reject.txt failed — {why}")),
1137                        }
1138                    }
1139                    | Err(why) => Err(eyre!("Write accept.txt failed — {why}")),
1140                }
1141            }
1142            | Err(why) => {
1143                error!(config = config_path.to_absolute_string(), "=> {} Vale sync — {why}", Label::fail());
1144                Err(why)
1145            }
1146        }
1147    }
1148    fn with_binary<P>(mut self, path: P) -> Self
1149    where
1150        P: Into<PathBuf>,
1151    {
1152        self.binary = Some(path.into());
1153        self
1154    }
1155    fn with_config(mut self, value: ValeConfig) -> Self {
1156        self.config = Some(value);
1157        self
1158    }
1159    fn with_system_command(mut self) -> Self {
1160        let name = self.command();
1161        if command_exists(name.clone()) {
1162            match which(name.clone()) {
1163                | Ok(path) => {
1164                    let path = path.to_path_buf();
1165                    self.binary = Some(path.clone());
1166                    match cmd!(name.clone(), ["--version"]) {
1167                        | Ok(output) => {
1168                            let stdout = output.stdout();
1169                            let version = stdout.strip_prefix("vale version ").unwrap_or(stdout.as_str()).trim().to_string();
1170                            self.version = Some(SemanticVersion::from(version.as_ref()));
1171                            debug!(
1172                                path = path.to_absolute_string(),
1173                                "=> {} System {} (v{version}) command",
1174                                Label::using(),
1175                                name.green().bold(),
1176                            );
1177                        }
1178                        | Err(why) => {
1179                            error!("=> {} Resolve {name} version — {why}", Label::fail());
1180                        }
1181                    }
1182                }
1183                | Err(why) => {
1184                    error!("=> {} Resolve {name} binary — {why}", Label::fail());
1185                }
1186            }
1187        }
1188        self
1189    }
1190    fn with_version(mut self, value: String) -> Self {
1191        self.version = Some(SemanticVersion::from(value.as_ref()));
1192        self
1193    }
1194}
1195#[async_trait]
1196impl StaticAnalyzerConfig for ValeConfig {
1197    async fn ini(self) -> Ini {
1198        let ValeConfig {
1199            packages,
1200            vocabularies,
1201            disabled,
1202            ..
1203        } = self;
1204        let package_names = packages.clone();
1205        let mut conf = Ini::new();
1206        let package_repository = Repository::GitLab {
1207            id: None,
1208            location: Location::Simple("https://code.ornl.gov/research-enablement/vale-package".to_string()),
1209        };
1210        let package_url = match package_repository.latest_release().await {
1211            | Some(release) => {
1212                let tag = release.tag_name;
1213                format!("https://code.ornl.gov/research-enablement/vale-package/-/archive/{tag}/vale-package-{tag}.zip")
1214            }
1215            | None => DEFAULT_VALE_PACKAGE_URL.to_string(),
1216        };
1217        let package_sources = package_names
1218            .iter()
1219            .map(Self::resolve_package)
1220            .chain(core::iter::once(package_url))
1221            .collect::<Vec<String>>();
1222        // CAUTION: Order of attributes in INI file matter. "StylesPath" must come before "Vocab"
1223        conf.with_section::<String>(None)
1224            .set("StylesPath", "styles")
1225            .set("Vocab", vocabularies.join(", "))
1226            .set("Packages", package_sources.join(", "));
1227        conf.with_section(Some("*")).set(
1228            "BasedOnStyles",
1229            format!("Vale, {}, {}", CUSTOM_VALE_PACKAGE_NAME, package_names.join(", ")),
1230        );
1231        disabled.iter().for_each(|rule| {
1232            conf.with_section(Some("*")).set(rule, "NO");
1233        });
1234        conf
1235    }
1236    fn resolve_package(value: impl AsRef<str>) -> String {
1237        let value = value.as_ref().trim();
1238        if is_uri_or_path(value) {
1239            value.to_string()
1240        } else {
1241            format!("https://github.com/errata-ai/{value}/releases/latest/download/{value}.zip")
1242        }
1243    }
1244    async fn save(self) -> ValeConfig {
1245        let path = self.clone().path;
1246        let parent = path.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
1247        match create_dir_all(parent.clone()) {
1248            | Ok(_) => {}
1249            | Err(why) => error!(directory = parent.to_absolute_string(), "=> {} Create — {why}", Label::fail()),
1250        }
1251        match self.clone().ini().await.write_to_file(path.clone()) {
1252            | Ok(_) => {
1253                debug!(path = path.to_absolute_string(), "=> {} Saved configuration", Label::using());
1254            }
1255            | Err(why) => {
1256                error!("=> {} Save configuration — {why}", Label::fail());
1257            }
1258        }
1259        self
1260    }
1261    fn with_path(mut self, path: PathBuf) -> Self {
1262        self.path = path;
1263        self
1264    }
1265}
1266pub(crate) fn collect_validation_checks<T>(value: &T) -> Vec<Check>
1267where
1268    T: Validate,
1269{
1270    value
1271        .validate()
1272        .err()
1273        .into_iter()
1274        .flat_map(|err| {
1275            let kind = ValidationErrorsKind::Struct(Box::new(err));
1276            process("", &kind).into_iter().map(|issue| {
1277                let validator_error = validator::ValidationError {
1278                    code: issue.code.to_string().into(),
1279                    message: Some(issue.message.clone().into()),
1280                    params: issue.params.clone().into_iter().map(|(key, value)| (key.into(), value)).collect(),
1281                };
1282                let errors = ValidationErrorsKind::Field(vec![validator_error]);
1283                let prefix = issue.path.clone().unwrap_or_else(|| issue.locator());
1284                check_err!(
1285                    CheckCategory::Schema,
1286                    locator: issue.locator(),
1287                    context: prefix,
1288                    message: issue.message,
1289                    errors: ErrorKind::Validator(errors)
1290                )
1291            })
1292        })
1293        .collect()
1294}
1295/// Convert Lychee response to [`Check`]
1296pub(crate) fn convert_lychee_response(value: Response) -> Check {
1297    let body = value.body().to_string();
1298    let url = value.source().to_string();
1299    debug!(url, "=> {} Response: {:#?}", Label::using(), body.dimmed().cyan());
1300    let status_code = value.status().code().map(|c| c.to_string()).unwrap_or_default();
1301    match value.status() {
1302        | Status::Ok(_) | Status::Redirected(_, _) => check!(
1303            CheckCategory::Link,
1304            true,
1305            severity: CheckSeverity::Info,
1306            status_code: status_code.clone(),
1307            message: "Has no HTTP errors"
1308        ),
1309        | Status::Cached(status) => match status {
1310            | CacheStatus::Ok(_) => check!(
1311                CheckCategory::Link,
1312                true,
1313                severity: CheckSeverity::Info,
1314                status_code: status_code.clone(),
1315                message: "Has no HTTP errors"
1316            ),
1317            | CacheStatus::Error(Some(_)) => check_err!(
1318                CheckCategory::Link,
1319                status_code: status_code.clone(),
1320                message: "Has cached HTTP errors"
1321            ),
1322            | CacheStatus::Unsupported => check!(
1323                CheckCategory::Link,
1324                false,
1325                severity: CheckSeverity::Warning,
1326                status_code: status_code.clone(),
1327                message: "Unsupported cached response"
1328            ),
1329            | _ => check!(
1330                CheckCategory::Link,
1331                true,
1332                severity: CheckSeverity::Suggestion,
1333                status_code: status_code.clone(),
1334                message: "Ignored or otherwise successful (cached response)"
1335            ),
1336        },
1337        | Status::Error(_) => check_err!(
1338            CheckCategory::Link,
1339            status_code: status_code.clone(),
1340            message: "Has HTTP errors"
1341        ),
1342        | Status::Unsupported(why) => check!(
1343            CheckCategory::Link,
1344            false,
1345            severity: CheckSeverity::Warning,
1346            status_code: status_code.clone(),
1347            message: format!("Unsupported HTTP response — {why}")
1348        ),
1349        | Status::UnknownStatusCode(_) => check!(
1350            CheckCategory::Link,
1351            false,
1352            severity: CheckSeverity::Warning,
1353            status_code: status_code.clone(),
1354            message: "Unknown HTTP response"
1355        ),
1356        | Status::Timeout(_) => check!(
1357            CheckCategory::Link,
1358            false,
1359            severity: CheckSeverity::Warning,
1360            status_code: status_code.clone(),
1361            message: "HTTP timeout"
1362        ),
1363        | _ => check!(
1364            CheckCategory::Link,
1365            true,
1366            severity: CheckSeverity::Suggestion,
1367            status_code: status_code.clone(),
1368            message: "Ignored or otherwise successful"
1369        ),
1370    }
1371}
1372/// Perform link check on given URL using Lychee
1373pub fn link_check<'a, T>(uri: Option<T>, locator: Option<String>) -> BoxFuture<'a, Check>
1374where
1375    T: Into<String> + Send + 'a,
1376{
1377    async move {
1378        match uri {
1379            | Some(value) => {
1380                let context = value.into();
1381                let result = lychee_lib::check(context.as_str()).await;
1382                match result {
1383                    | Ok(response) => convert_lychee_response(response).with_context(context).with_locator(locator),
1384                    | Err(_) => check_err!(CheckCategory::Link, context: context, message: "Unreachable").with_locator(locator),
1385                }
1386            }
1387            | None => check_err!(CheckCategory::Link, message: "Missing URL").with_locator(locator),
1388        }
1389    }
1390    .boxed()
1391}
1392/// Prints `text` to stdout using syntax highlighting for the specified `syntax`.
1393///
1394/// `highlight` is an iterator of line numbers to highlight in the output.
1395#[cfg(feature = "analysis")]
1396pub fn pretty_print<I: IntoIterator<Item = usize>>(text: &str, syntax: ProgrammingLanguage, highlight: I) {
1397    let input = format!("{text}\n");
1398    let language = syntax.to_string();
1399    let mut printer = PrettyPrinter::new();
1400    printer
1401        .input_from_bytes(input.as_bytes())
1402        .theme("zenburn")
1403        .language(&language)
1404        .line_numbers(true);
1405    for line in highlight {
1406        printer.highlight(line);
1407    }
1408    #[allow(clippy::unwrap_used)]
1409    printer.print().unwrap();
1410}
1411/// Convert vector of values of a given type to a Polars [DataFrame]
1412/// ### Example
1413/// ```ignore
1414/// let df = to_dataframe::<i32, _, str>(vec![1, 2, 3], ["a", "b", "c"]);
1415/// ```
1416///
1417/// [DataFrame]: https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html
1418#[cfg(feature = "analysis")]
1419pub fn to_dataframe<'a, T, I, H>(values: Vec<T>, names: I) -> PolarsResult<DataFrame>
1420where
1421    T: IntoRow<'a>,
1422    H: Into<PlSmallStr>,
1423    I: IntoIterator<Item = H>,
1424{
1425    let rows = values.into_iter().map(|value| value.to_row::<T>()).collect::<Vec<_>>();
1426    match DataFrame::from_rows(&rows) {
1427        | Ok(mut df) => match df.set_column_names(&names.into_iter().map(Into::into).collect::<Vec<PlSmallStr>>()) {
1428            | Ok(_) => Ok(df),
1429            | Err(why) => Err(why),
1430        },
1431        | Err(why) => Err(why),
1432    }
1433}
1434
1435#[cfg(test)]
1436mod tests;