rudof_lib 0.3.23

RDF and Knowledge Graphs processing tool
use crate::{
    Result, Rudof,
    errors::{DataError, IriError},
    formats::{IriNormalizationMode, QueryType},
};
#[cfg(not(target_family = "wasm"))]
use crossterm::terminal;
use prefixmap::IriRef;
use rudof_iri::IriS;
use rudof_rdf::rdf_core::{NeighsRDF, query::SparqlQuery};
use shex_ast::{ShapeMapParser, shapemap::NodeSelector};
#[cfg(not(target_family = "wasm"))]
use std::env;
use std::str::FromStr;
#[cfg(not(target_family = "wasm"))]
use url::Url;

/// Normalizes a node/shape IRI string for `ShapeMapParser` according to `mode`.
///
/// - `Lax`: wraps strings that look like bare absolute IRIs (contain `://`, no leading `<`, `_`,
///   or `{`) in angle brackets. Heuristic — see [`IriNormalizationMode`] docs for edge cases.
/// - `Strict`: returns the trimmed string unchanged; bare IRIs will produce a parser error.
pub(crate) fn normalize_iri_str(s: &str, mode: IriNormalizationMode) -> String {
    let trimmed = s.trim();
    match mode {
        IriNormalizationMode::Strict => trimmed.to_string(),
        IriNormalizationMode::Lax => {
            let is_bare_iri = !trimmed.starts_with('<')
                && !trimmed.starts_with('_')
                && !trimmed.starts_with('{')
                && trimmed.contains("://");
            if is_bare_iri {
                format!("<{}>", trimmed)
            } else {
                trimmed.to_string()
            }
        },
    }
}

pub fn get_base_iri(rudof: &mut Rudof, base_iri: Option<&str>) -> Result<IriS> {
    if let Some(base_iri) = base_iri {
        let base_iri = IriS::from_str(base_iri).map_err(|error| IriError::ParseError {
            iri: base_iri.to_string(),
            error: error.to_string(),
        })?;

        Ok(base_iri.clone())
    } else if let Some(base_iri) = rudof.config.shex().base() {
        Ok(base_iri.clone())
    } else {
        // There is no current directory to default to on wasm.
        #[cfg(target_family = "wasm")]
        return Err(IriError::WasmNotSupported {
            operation: "defaulting the base IRI to the current directory (provide a base IRI)".to_string(),
        }
        .into());
        #[cfg(not(target_family = "wasm"))]
        {
            let cwd = env::current_dir().map_err(|e| IriError::PathConversionError {
                path: ".".to_string(),
                error: format!("Error resolving base IRI. Failed to get current directory: {e}"),
            })?;

            let url = Url::from_directory_path(&cwd).map_err(|_| IriError::PathConversionError {
                path: cwd.to_string_lossy().to_string(),
                error: "Error resolving base IRI. Cannot convert current directory to a file URL".to_string(),
            })?;
            Ok(url.into())
        }
    }
}

/// Whether tables and other text output use colors and hyperlinks (ANSI
/// escape codes). There is no terminal on wasm, where the output is usually
/// shown in a web page and the escape codes would appear as stray characters.
pub fn terminal_colors() -> bool {
    !cfg!(target_family = "wasm")
}

#[cfg(not(target_family = "wasm"))]
const MAX_TERMINAL_WIDTH: usize = 100;
const DEFAULT_TERMINAL_WIDTH: usize = 80;

/// Width of the terminal, used to lay out tables. There is no terminal on
/// wasm, so the default width is used there.
#[cfg(target_family = "wasm")]
pub fn terminal_width() -> usize {
    DEFAULT_TERMINAL_WIDTH
}

#[cfg(not(target_family = "wasm"))]
pub fn terminal_width() -> usize {
    if let Ok((cols, _)) = terminal::size() {
        sanitize_width(cols as usize)
    } else {
        DEFAULT_TERMINAL_WIDTH
    }
}

#[cfg(not(target_family = "wasm"))]
fn sanitize_width(width: usize) -> usize {
    match width {
        w if w > MAX_TERMINAL_WIDTH => MAX_TERMINAL_WIDTH,
        w if w < 40 => DEFAULT_TERMINAL_WIDTH,
        w => w,
    }
}

// Detect query type from SPARQL string
pub fn detect_query_type(query: &SparqlQuery) -> QueryType {
    if query.is_select() {
        QueryType::Select
    } else if query.is_construct() {
        QueryType::Construct
    } else if query.is_ask() {
        QueryType::Ask
    } else {
        QueryType::Describe
    }
}

/// Parses a node selector string into a `NodeSelector` instance.
pub fn parse_node_selector(node: &str, iri_mode: IriNormalizationMode) -> Result<NodeSelector> {
    let normalized = normalize_iri_str(node, iri_mode);
    ShapeMapParser::parse_node_selector(normalized.as_str()).map_err(|e| {
        Box::new(DataError::FailedNodeSelectorParse {
            node: normalized.as_str().to_string(),
            error: e.to_string(),
        })
        .into()
    })
}

/// Converts predicate strings to IRI objects
pub fn convert_predicate_strings_to_iris<S>(predicates: &[String], rdf: &S) -> Result<Vec<S::IRI>>
where
    S: NeighsRDF,
{
    predicates
        .iter()
        .map(|pred_str| {
            let iri_ref = parse_iri_ref(pred_str)?;

            let iri = match iri_ref {
                IriRef::Prefixed { prefix, local } => {
                    rdf.resolve_prefix_local(prefix.as_str(), local.as_str()).map_err(|e| {
                        Box::new(DataError::FailedPrefixResolution {
                            prefix: prefix.to_string(),
                            error: e.to_string(),
                        })
                    })?
                },
                IriRef::Iri(iri) => iri,
            };

            Ok(iri.into())
        })
        .collect()
}

/// Parses an IRI string into an `IriRef` instance.
fn parse_iri_ref(iri: &str) -> Result<IriRef> {
    ShapeMapParser::parse_iri_ref(iri).map_err(|e| {
        Box::new(DataError::FailedIriRefParse {
            iri: iri.to_string(),
            error: e.to_string(),
        })
        .into()
    })
}