use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Deserialize)]
pub struct GspParams {
pub graph: Option<String>,
pub default: Option<String>,
}
impl GspParams {
pub fn default_graph() -> Self {
Self {
graph: None,
default: Some(String::new()),
}
}
pub fn named_graph(uri: impl Into<String>) -> Self {
Self {
graph: Some(uri.into()),
default: None,
}
}
pub fn is_default_graph(&self) -> bool {
self.default.is_some() || self.graph.as_deref() == Some("default")
}
pub fn is_union_graph(&self) -> bool {
self.graph.as_deref() == Some("union")
}
pub fn graph_uri(&self) -> Option<&str> {
self.graph
.as_deref()
.filter(|g| *g != "default" && *g != "union")
}
}
#[derive(Debug, Clone)]
pub enum GraphTarget {
Default,
Union,
Named(String),
}
impl GraphTarget {
pub fn from_params(params: &GspParams) -> Result<Self, GspError> {
if params.is_default_graph() {
Ok(GraphTarget::Default)
} else if params.is_union_graph() {
Ok(GraphTarget::Union)
} else if let Some(uri) = params.graph_uri() {
Ok(GraphTarget::Named(uri.to_string()))
} else {
Ok(GraphTarget::Default)
}
}
pub fn label(&self) -> String {
match self {
GraphTarget::Default => "default graph".to_string(),
GraphTarget::Union => "union graph".to_string(),
GraphTarget::Named(uri) => format!("graph <{}>", uri),
}
}
pub fn is_writable(&self) -> bool {
match self {
GraphTarget::Default | GraphTarget::Named(_) => true,
GraphTarget::Union => false, }
}
}
impl fmt::Display for GraphTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.label())
}
}
pub use oxirs_core::parser::RdfFormat;
pub trait RdfFormatExt {
fn from_media_type_gsp(media_type: &str) -> Option<RdfFormat>;
fn default_order_gsp() -> Vec<RdfFormat>;
}
impl RdfFormatExt for RdfFormat {
fn from_media_type_gsp(media_type: &str) -> Option<RdfFormat> {
let media_type = media_type.split(';').next()?.trim().to_lowercase();
match media_type.as_str() {
"text/turtle" | "application/x-turtle" => Some(RdfFormat::Turtle),
"application/n-triples" | "text/plain" => Some(RdfFormat::NTriples),
"application/n-quads" => Some(RdfFormat::NQuads),
"application/rdf+xml" | "application/xml" => Some(RdfFormat::RdfXml),
"application/ld+json" | "application/json" => Some(RdfFormat::JsonLd),
"application/trig" => Some(RdfFormat::TriG),
_ => None,
}
}
fn default_order_gsp() -> Vec<RdfFormat> {
vec![
RdfFormat::Turtle,
RdfFormat::JsonLd,
RdfFormat::NTriples,
RdfFormat::RdfXml,
RdfFormat::TriG,
RdfFormat::NQuads,
]
}
}
#[derive(Debug, thiserror::Error)]
pub enum GspError {
#[error("Graph not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Method not allowed: {0}")]
MethodNotAllowed(String),
#[error("Unsupported media type: {0}")]
UnsupportedMediaType(String),
#[error("Not acceptable: no suitable content type available")]
NotAcceptable,
#[error("Parse error: {0}")]
ParseError(String),
#[error("Store error: {0}")]
StoreError(String),
#[error("Internal error: {0}")]
Internal(String),
}
impl GspError {
pub fn status_code(&self) -> u16 {
match self {
GspError::NotFound(_) => 404,
GspError::BadRequest(_) => 400,
GspError::MethodNotAllowed(_) => 405,
GspError::UnsupportedMediaType(_) => 415,
GspError::NotAcceptable => 406,
GspError::ParseError(_) => 400,
GspError::StoreError(_) => 500,
GspError::Internal(_) => 500,
}
}
}
impl axum::response::IntoResponse for GspError {
fn into_response(self) -> axum::response::Response {
use axum::http::StatusCode;
use axum::response::Json;
let status =
StatusCode::from_u16(self.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let message = self.to_string();
(
status,
Json(serde_json::json!({
"error": message,
"status": status.as_u16(),
})),
)
.into_response()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct GspStats {
pub triples: usize,
pub duration_ms: u64,
pub graph: String,
pub operation: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gsp_params_default_graph() {
let params = GspParams::default_graph();
assert!(params.is_default_graph());
assert!(!params.is_union_graph());
assert_eq!(params.graph_uri(), None);
}
#[test]
fn test_gsp_params_named_graph() {
let params = GspParams::named_graph("http://example.org/graph1");
assert!(!params.is_default_graph());
assert!(!params.is_union_graph());
assert_eq!(params.graph_uri(), Some("http://example.org/graph1"));
}
#[test]
fn test_graph_target_from_params() {
let params = GspParams::default_graph();
let target = GraphTarget::from_params(¶ms).unwrap();
assert!(matches!(target, GraphTarget::Default));
let params = GspParams {
graph: Some("union".to_string()),
default: None,
};
let target = GraphTarget::from_params(¶ms).unwrap();
assert!(matches!(target, GraphTarget::Union));
}
#[test]
fn test_graph_target_writable() {
assert!(GraphTarget::Default.is_writable());
assert!(GraphTarget::Named("http://example.org/g1".to_string()).is_writable());
assert!(!GraphTarget::Union.is_writable());
}
#[test]
fn test_rdf_format_media_types() {
assert_eq!(RdfFormat::Turtle.media_type(), "text/turtle");
assert_eq!(RdfFormat::JsonLd.media_type(), "application/ld+json");
}
#[test]
fn test_rdf_format_from_media_type() {
assert_eq!(
RdfFormat::from_media_type_gsp("text/turtle"),
Some(RdfFormat::Turtle)
);
assert_eq!(
RdfFormat::from_media_type_gsp("application/ld+json; charset=utf-8"),
Some(RdfFormat::JsonLd)
);
assert_eq!(RdfFormat::from_media_type_gsp("unknown/type"), None);
}
}