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