mod csv;
mod json;
mod markdown;
mod txt;
use async_std::io::{ErrorKind, Write};
use async_trait::async_trait;
use cli_utils::StreamIdent;
use std::str::FromStr;
use async_std::io;
use clap::{ValueEnum, builder::PossibleValue};
use serde::{Deserialize, Serialize};
use crate::{
BoxError,
anchor::{self, Anchor},
config::Tool as Config,
link::Link,
};
type Writer = Box<dyn Write + Unpin + Send + Sync + 'static>;
type WriterOpt = Option<Writer>;
const EXT_TEXT: &str = "txt";
const EXT_MARKDOWN: &str = "md";
const EXT_CSV: &str = "csv";
const EXT_TSV: &str = "tsv";
const EXT_JSON: &str = "json";
const EXT_RDF_TURTLE: &str = "ttl";
const ALL_EXTS: [&str; 6] = [
EXT_TEXT,
EXT_MARKDOWN,
EXT_CSV,
EXT_TSV,
EXT_JSON,
EXT_RDF_TURTLE,
];
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Type {
#[default]
Text,
Markdown,
Csv,
Tsv,
Json,
RdfTurtle,
}
impl ValueEnum for Type {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Text,
Self::Markdown,
Self::Csv,
Self::Tsv,
Self::Json,
Self::RdfTurtle,
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(self.as_str().into())
}
}
impl Type {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Text => EXT_TEXT,
Self::Markdown => EXT_MARKDOWN,
Self::Csv => EXT_CSV,
Self::Tsv => EXT_TSV,
Self::Json => EXT_JSON,
Self::RdfTurtle => EXT_RDF_TURTLE,
}
}
}
impl FromStr for Type {
type Err = std::io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
EXT_TEXT | "text" | "plain" | "grep" => Self::Text,
EXT_MARKDOWN | "markdown" => Self::Markdown,
EXT_CSV => Self::Csv,
EXT_JSON => Self::Json,
EXT_RDF_TURTLE | "turtle" | "rdf" | "rdf-turtle" => Self::RdfTurtle,
_ => Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"Invalid result format given: '{}' \nValid formats are: {}",
s,
ALL_EXTS.join(", ")
),
))?,
})
}
}
#[allow(clippy::ref_option)]
async fn construct_out_stream_opt(
specifier_opt: &Option<StreamIdent>,
) -> io::Result<Option<Box<dyn io::Write + Unpin + Send + Sync>>> {
match specifier_opt.as_ref() {
None => Ok(None),
Some(specifier) => Ok(Some(specifier.create_output_writer().await?)),
}
}
pub fn write_to_stderr(errors: &[BoxError]) {
for error in errors {
log::error!("{error:#?}");
}
}
pub async fn sink(
config: &Config,
links: &[Link],
anchors: &[Anchor],
errors: &[BoxError],
) -> io::Result<()> {
let sink_init = match config.result_format {
Type::Text => txt::Sink::init,
Type::Json => json::Sink::init,
Type::Markdown => markdown::Sink::init,
Type::Csv | Type::Tsv => csv::Sink::init,
Type::RdfTurtle => Err(std::io::Error::new(
ErrorKind::InvalidInput,
"Result format not yet supported",
))?,
};
let links_writer = construct_out_stream_opt(&config.links).await?;
let anchors_writer = construct_out_stream_opt(&config.anchors).await?;
let mut sink = sink_init(config.result_format, config, links_writer, anchors_writer).await?;
for link in links {
sink.sink_link(link).await?;
}
for anchor in anchors {
sink.sink_anchor(anchor).await?;
}
for error in errors {
sink.sink_error(error).await?;
}
sink.finalize().await
}
#[async_trait]
pub trait Sink: Send + Sync {
async fn init(
format: Type,
config: &Config,
links_stream: WriterOpt,
anchors_stream: WriterOpt,
) -> io::Result<Box<dyn Sink>>
where
Self: Sized;
async fn sink_link(&mut self, link: &Link) -> io::Result<()>;
async fn sink_anchor(&mut self, anchor: &Anchor) -> io::Result<()>;
async fn sink_error(&mut self, error: &BoxError) -> io::Result<()> {
log::error!("{error:#?}");
Ok(())
}
async fn finalize(&mut self) -> io::Result<()>;
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
struct LinkExtendedRec<'a> {
src_file: String,
src_line: usize,
src_column: usize,
src_is_file_system: bool,
src_is_url: bool,
src_is_local: bool,
src_is_remote: bool,
trg_link: String,
trg_fragment: Option<&'a str>,
trg_is_file_system: bool,
trg_is_url: bool,
trg_is_local: bool,
trg_is_remote: bool,
}
#[derive(Debug, Serialize)]
struct LinkSimpleRec<'a> {
src_file: String,
src_line: usize,
src_column: usize,
trg_link: String,
trg_fragment: Option<&'a str>,
}
#[derive(Debug)]
enum LinkRec<'a> {
Simple(LinkSimpleRec<'a>),
Extended(LinkExtendedRec<'a>),
}
impl<'a> LinkRec<'a> {
fn new(lnk: &'a Link, extended: bool) -> Self {
if extended {
Self::Extended(LinkExtendedRec {
src_file: lnk.source.file.to_string(),
src_line: lnk.source.pos.line,
src_column: lnk.source.pos.column,
src_is_file_system: lnk.source.file.is_file_system(),
src_is_url: lnk.source.file.is_url(),
src_is_local: lnk.source.file.is_local(),
src_is_remote: lnk.source.file.is_remote(),
trg_link: lnk.target.without_fragment().to_string(),
trg_fragment: lnk.target.fragment(),
trg_is_file_system: lnk.target.is_file_system(),
trg_is_url: lnk.target.is_url(),
trg_is_local: lnk.target.is_local(),
trg_is_remote: lnk.target.is_remote(),
})
} else {
Self::Simple(LinkSimpleRec {
src_file: lnk.source.file.to_string(),
src_line: lnk.source.pos.line,
src_column: lnk.source.pos.column,
trg_link: lnk.target.without_fragment().to_string(),
trg_fragment: lnk.target.fragment(),
})
}
}
}
impl Serialize for LinkRec<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Simple(rec) => rec.serialize(serializer),
Self::Extended(rec) => rec.serialize(serializer),
}
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
struct LinkExtendedOwnedRec {
src_file: String,
src_line: usize,
src_column: usize,
src_is_file_system: bool,
src_is_url: bool,
src_is_local: bool,
src_is_remote: bool,
trg_link: String,
trg_fragment: Option<String>,
trg_is_file_system: bool,
trg_is_url: bool,
trg_is_local: bool,
trg_is_remote: bool,
}
#[derive(Debug, Serialize)]
struct LinkSimpleOwnedRec {
src_file: String,
src_line: usize,
src_column: usize,
trg_link: String,
trg_fragment: Option<String>,
}
#[derive(Debug)]
enum LinkOwnedRec {
Simple(LinkSimpleOwnedRec),
Extended(LinkExtendedOwnedRec),
}
impl LinkOwnedRec {
fn new(lnk: &Link, extended: bool) -> Self {
if extended {
Self::Extended(LinkExtendedOwnedRec {
src_file: lnk.source.file.to_string(),
src_line: lnk.source.pos.line,
src_column: lnk.source.pos.column,
src_is_file_system: lnk.source.file.is_file_system(),
src_is_url: lnk.source.file.is_url(),
src_is_local: lnk.source.file.is_local(),
src_is_remote: lnk.source.file.is_remote(),
trg_link: lnk.target.without_fragment().to_string(),
trg_fragment: lnk.target.fragment().map(ToOwned::to_owned),
trg_is_file_system: lnk.target.is_file_system(),
trg_is_url: lnk.target.is_url(),
trg_is_local: lnk.target.is_local(),
trg_is_remote: lnk.target.is_remote(),
})
} else {
Self::Simple(LinkSimpleOwnedRec {
src_file: lnk.source.file.to_string(),
src_line: lnk.source.pos.line,
src_column: lnk.source.pos.column,
trg_link: lnk.target.without_fragment().to_string(),
trg_fragment: lnk.target.fragment().map(ToOwned::to_owned),
})
}
}
}
impl Serialize for LinkOwnedRec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Simple(rec) => rec.serialize(serializer),
Self::Extended(rec) => rec.serialize(serializer),
}
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
struct AnchorExtendedRec<'a> {
src_file: String,
src_line: usize,
src_column: usize,
src_is_file_system: bool,
src_is_url: bool,
src_is_local: bool,
src_is_remote: bool,
name: &'a str,
r#type: anchor::Type,
}
#[derive(Debug, Serialize)]
struct AnchorSimpleRec<'a> {
src_file: String,
src_line: usize,
src_column: usize,
name: &'a str,
}
#[derive(Debug)]
enum AnchorRec<'a> {
Simple(AnchorSimpleRec<'a>),
Extended(AnchorExtendedRec<'a>),
}
impl Serialize for AnchorRec<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Simple(rec) => rec.serialize(serializer),
Self::Extended(rec) => rec.serialize(serializer),
}
}
}
impl<'a> AnchorRec<'a> {
fn new(anchor: &'a Anchor, extended: bool) -> Self {
if extended {
Self::Extended(AnchorExtendedRec {
src_file: anchor.source.file.to_string(),
src_line: anchor.source.pos.line,
src_column: anchor.source.pos.column,
src_is_file_system: anchor.source.file.is_file_system(),
src_is_url: anchor.source.file.is_url(),
src_is_local: anchor.source.file.is_local(),
src_is_remote: anchor.source.file.is_remote(),
name: &anchor.name,
r#type: anchor.r#type,
})
} else {
Self::Simple(AnchorSimpleRec {
src_file: anchor.source.file.to_string(),
src_line: anchor.source.pos.line,
src_column: anchor.source.pos.column,
name: &anchor.name,
})
}
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize)]
struct AnchorExtendedOwnedRec {
src_file: String,
src_line: usize,
src_column: usize,
src_is_file_system: bool,
src_is_url: bool,
src_is_local: bool,
src_is_remote: bool,
name: String,
r#type: anchor::Type,
}
#[derive(Debug, Serialize)]
struct AnchorSimpleOwnedRec {
src_file: String,
src_line: usize,
src_column: usize,
name: String,
}
#[derive(Debug)]
enum AnchorOwnedRec {
Simple(AnchorSimpleOwnedRec),
Extended(AnchorExtendedOwnedRec),
}
impl Serialize for AnchorOwnedRec {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Simple(rec) => rec.serialize(serializer),
Self::Extended(rec) => rec.serialize(serializer),
}
}
}
impl AnchorOwnedRec {
fn new(anchor: &Anchor, extended: bool) -> Self {
if extended {
Self::Extended(AnchorExtendedOwnedRec {
src_file: anchor.source.file.to_string(),
src_line: anchor.source.pos.line,
src_column: anchor.source.pos.column,
src_is_file_system: anchor.source.file.is_file_system(),
src_is_url: anchor.source.file.is_url(),
src_is_local: anchor.source.file.is_local(),
src_is_remote: anchor.source.file.is_remote(),
name: anchor.name.clone(),
r#type: anchor.r#type,
})
} else {
Self::Simple(AnchorSimpleOwnedRec {
src_file: anchor.source.file.to_string(),
src_line: anchor.source.pos.line,
src_column: anchor.source.pos.column,
name: anchor.name.clone(),
})
}
}
}