1use 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#[async_trait]
64pub trait StaticAnalyzer<Config: StaticAnalyzerConfig> {
65 fn command(&self) -> String;
67 async fn download(self, config: Option<Config>, skip_verify_checksum: bool) -> Self;
69 async fn download_checksums(self) -> ApiResult<HashMap<String, String>>;
71 fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf;
73 async fn resolve(_config: Config, _is_offline: bool, _skip_verify_checksum: bool) -> Self;
75 async fn run(&self, path: PathBuf, content: String, output: Option<String>) -> Vec<Check>;
77 async fn sync(self, is_offline: bool, quiet: bool) -> ApiResult<()>;
79 fn with_binary<P>(self, path: P) -> Self
81 where
82 P: Into<PathBuf>;
83 fn with_config(self, value: Config) -> Self;
85 fn with_system_command(self) -> Self;
87 fn with_version(self, value: String) -> Self;
89}
90#[async_trait]
92pub trait StaticAnalyzerConfig {
93 async fn ini(self) -> Ini;
95 async fn save(self) -> Self;
97 fn with_path(self, path: PathBuf) -> Self;
99 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 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 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}
1295pub(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}
1372pub 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#[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#[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;