better_sparql_client 0.1.0

SPARQL client built on the oxigraph ecosystem
Documentation
use oxrdf::Graph;
use oxttl::TurtleParseError;
use sparesults::{
    QueryResultsFormat, QueryResultsParseError, QueryResultsParser, QuerySolution,
    ReaderQueryResultsParserOutput,
};
use url::Url;

use crate::utils::parse_ttl_into_graph;

#[derive(Debug, thiserror::Error)]
pub enum SelectError {
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Parse error: {0}")]
    Parse(#[from] QueryResultsParseError),

    #[error("Unexpected query results format. Expected query solutions, got boolean.")]
    UnexpectedFormat,
}

#[derive(Debug, thiserror::Error)]
pub enum AskError {
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Parse error: {0}")]
    Parse(#[from] QueryResultsParseError),

    #[error("Unexpected query results format. Expected query solutions, got boolean.")]
    UnexpectedFormat,
}

#[derive(Debug, thiserror::Error)]
pub enum SelectGraphError {
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Parse error: {0}")]
    Parse(#[from] TurtleParseError),
}

#[derive(Debug, thiserror::Error)]
pub enum UpdateError {
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("SPARQL update failed with status: {0}")]
    FailedToUpdate(reqwest::StatusCode),
}

pub struct SparqlClient {
    http_client: reqwest::Client,
    service_url: Url,
}

impl SparqlClient {
    pub fn new(service_url: Url) -> Self {
        Self {
            http_client: reqwest::Client::new(),
            service_url,
        }
    }

    pub fn with_http_client(service_url: Url, http_client: reqwest::Client) -> Self {
        Self {
            http_client,
            service_url,
        }
    }

    fn build_query_request(&self, query: &str) -> reqwest::RequestBuilder {
        self.http_client
            .post(self.service_url.clone())
            .header("Content-Type", "application/sparql-query")
            .body(query.to_owned())
    }

    // query operations

    /// Executes a SELECT query and returns the results as a vector of QuerySolution.
    pub async fn select(&self, query: &str) -> Result<Vec<QuerySolution>, SelectError> {
        let response = self
            .build_query_request(query)
            .header("Accept", "application/sparql-results+json")
            .send()
            .await?;

        let body_bytes = response.bytes().await?;
        let query_results_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);

        let ReaderQueryResultsParserOutput::Solutions(solutions) =
            query_results_parser.for_reader(&*body_bytes)?
        else {
            return Err(SelectError::UnexpectedFormat);
        };

        let solutions = solutions.into_iter().flatten().collect();

        Ok(solutions)
    }

    /// Executes an ASK query and returns the boolean result.
    pub async fn ask(&self, query: &str) -> Result<bool, AskError> {
        let response = self
            .build_query_request(query)
            .header("Accept", "application/sparql-results+json")
            .send()
            .await?;

        let body_bytes = response.bytes().await?;

        let query_results_parser = QueryResultsParser::from_format(QueryResultsFormat::Json);

        let ReaderQueryResultsParserOutput::Boolean(bool) =
            query_results_parser.for_reader(&*body_bytes)?
        else {
            return Err(AskError::UnexpectedFormat);
        };

        Ok(bool)
    }

    /// Executes a CONSTRUCT or DESCRIBE query and returns the resulting graph.
    pub async fn select_graph(&self, query: &str) -> Result<Graph, SelectGraphError> {
        let response = self
            .build_query_request(query)
            .header("Accept", "text/turtle")
            .send()
            .await?;

        let body = response.text().await?;
        let graph = parse_ttl_into_graph(&body)?;

        Ok(graph)
    }

    // update operations

    /// Executes an UPDATE query (INSERT, DELETE, etc.)
    /// Returns a result indicating success or failure of the update operation.
    pub async fn update(&self, query: &str) -> Result<(), UpdateError> {
        let response = self.build_query_request(query).send().await?;

        if !response.status().is_success() {
            return Err(UpdateError::FailedToUpdate(response.status()));
        }

        Ok(())
    }
}