use crate::utils::error::{Error, Result};
use std::marker::PhantomData;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Iri(String);
impl Iri {
#[inline]
pub fn new(iri: impl AsRef<str>) -> Result<Self> {
let iri_str = iri.as_ref();
if iri_str.contains(['<', '>', '"', '{', '}', '|', '^', '`', '\\']) {
return Err(Error::new(&format!(
"Invalid IRI format: contains forbidden characters: {}",
iri_str
)));
}
Ok(Self(iri_str.to_string()))
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variable(String);
impl Variable {
#[inline]
pub fn new(name: impl AsRef<str>) -> Result<Self> {
let name_str = name.as_ref();
let name_clean = name_str.strip_prefix('?').unwrap_or(name_str);
if !name_clean.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(Error::new(&format!(
"Invalid variable name: {}. Must be alphanumeric with underscores.",
name_str
)));
}
if name_clean.is_empty() {
return Err(Error::new("Variable name cannot be empty"));
}
Ok(Self(name_clean.to_string()))
}
#[inline]
pub fn as_str(&self) -> String {
format!("?{}", self.0)
}
#[inline]
pub fn name(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Literal(String);
impl Literal {
#[inline]
pub fn new(value: impl AsRef<str>) -> Self {
let escaped = value
.as_ref()
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
Self(escaped)
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
pub struct Building;
#[derive(Debug)]
pub struct WithVars;
#[derive(Debug)]
pub struct WithWhere;
#[derive(Debug)]
pub struct Complete;
#[derive(Debug)]
pub struct Select;
#[derive(Debug)]
pub struct Construct;
#[derive(Debug)]
pub struct Ask;
#[derive(Debug)]
pub struct Describe;
#[derive(Debug)]
pub struct SparqlQueryBuilder<Q, S> {
query_type: PhantomData<Q>,
state: PhantomData<S>,
prefixes: Vec<(String, String)>,
vars: Vec<String>,
construct_patterns: Vec<String>,
where_patterns: Vec<String>,
filters: Vec<String>,
order_by: Vec<String>,
limit_value: Option<usize>,
offset_value: Option<usize>,
distinct: bool,
}
impl<Q, S> SparqlQueryBuilder<Q, S> {
#[inline]
pub fn prefix(mut self, prefix: impl AsRef<str>, iri: Iri) -> Self {
self.prefixes
.push((prefix.as_ref().to_string(), iri.as_str().to_string()));
self
}
}
impl SparqlQueryBuilder<Select, Building> {
#[inline]
pub fn select() -> Self {
Self {
query_type: PhantomData,
state: PhantomData,
prefixes: Vec::new(),
vars: Vec::new(),
construct_patterns: Vec::new(),
where_patterns: Vec::new(),
filters: Vec::new(),
order_by: Vec::new(),
limit_value: None,
offset_value: None,
distinct: false,
}
}
#[inline]
pub fn var(mut self, var: Variable) -> SparqlQueryBuilder<Select, WithVars> {
self.vars.push(var.as_str());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
#[inline]
pub fn all_vars(mut self) -> SparqlQueryBuilder<Select, WithVars> {
self.vars.push("*".to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
#[inline]
pub fn distinct(mut self) -> Self {
self.distinct = true;
self
}
}
impl SparqlQueryBuilder<Select, WithVars> {
#[inline]
pub fn var(mut self, var: Variable) -> Self {
self.vars.push(var.as_str());
self
}
#[inline]
pub fn where_pattern(
mut self, pattern: impl AsRef<str>,
) -> SparqlQueryBuilder<Select, WithWhere> {
self.where_patterns.push(pattern.as_ref().to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
}
impl SparqlQueryBuilder<Select, WithWhere> {
#[inline]
pub fn where_pattern(mut self, pattern: impl AsRef<str>) -> Self {
self.where_patterns.push(pattern.as_ref().to_string());
self
}
#[inline]
pub fn filter(mut self, filter: impl AsRef<str>) -> Self {
self.filters.push(filter.as_ref().to_string());
self
}
#[inline]
pub fn order_by(mut self, var: Variable) -> Self {
self.order_by.push(var.as_str());
self
}
#[inline]
pub fn limit(mut self, limit: usize) -> Self {
self.limit_value = Some(limit);
self
}
#[inline]
pub fn offset(mut self, offset: usize) -> Self {
self.offset_value = Some(offset);
self
}
#[inline]
pub fn build(self) -> Result<String> {
let mut query = String::new();
for (prefix, iri) in &self.prefixes {
query.push_str(&format!("PREFIX {}: <{}>\n", prefix, iri));
}
if self.distinct {
query.push_str("SELECT DISTINCT ");
} else {
query.push_str("SELECT ");
}
query.push_str(&self.vars.join(" "));
query.push('\n');
query.push_str("WHERE {\n");
for pattern in &self.where_patterns {
query.push_str(" ");
query.push_str(pattern);
query.push_str(" .\n");
}
for filter in &self.filters {
query.push_str(" FILTER (");
query.push_str(filter);
query.push_str(")\n");
}
query.push_str("}\n");
if !self.order_by.is_empty() {
query.push_str("ORDER BY ");
query.push_str(&self.order_by.join(" "));
query.push('\n');
}
if let Some(limit) = self.limit_value {
query.push_str(&format!("LIMIT {}\n", limit));
}
if let Some(offset) = self.offset_value {
query.push_str(&format!("OFFSET {}\n", offset));
}
Ok(query)
}
}
impl SparqlQueryBuilder<Construct, Building> {
#[inline]
pub fn construct() -> Self {
Self {
query_type: PhantomData,
state: PhantomData,
prefixes: Vec::new(),
vars: Vec::new(),
construct_patterns: Vec::new(),
where_patterns: Vec::new(),
filters: Vec::new(),
order_by: Vec::new(),
limit_value: None,
offset_value: None,
distinct: false,
}
}
#[inline]
pub fn construct_pattern(
mut self, pattern: impl AsRef<str>,
) -> SparqlQueryBuilder<Construct, WithVars> {
self.construct_patterns.push(pattern.as_ref().to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
}
impl SparqlQueryBuilder<Construct, WithVars> {
#[inline]
pub fn construct_pattern(mut self, pattern: impl AsRef<str>) -> Self {
self.construct_patterns.push(pattern.as_ref().to_string());
self
}
#[inline]
pub fn where_pattern(
mut self, pattern: impl AsRef<str>,
) -> SparqlQueryBuilder<Construct, WithWhere> {
self.where_patterns.push(pattern.as_ref().to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
}
impl SparqlQueryBuilder<Construct, WithWhere> {
#[inline]
pub fn where_pattern(mut self, pattern: impl AsRef<str>) -> Self {
self.where_patterns.push(pattern.as_ref().to_string());
self
}
#[inline]
pub fn filter(mut self, filter: impl AsRef<str>) -> Self {
self.filters.push(filter.as_ref().to_string());
self
}
#[inline]
pub fn build(self) -> Result<String> {
let mut query = String::new();
for (prefix, iri) in &self.prefixes {
query.push_str(&format!("PREFIX {}: <{}>\n", prefix, iri));
}
query.push_str("CONSTRUCT {\n");
for pattern in &self.construct_patterns {
query.push_str(" ");
query.push_str(pattern);
query.push_str(" .\n");
}
query.push_str("}\n");
query.push_str("WHERE {\n");
for pattern in &self.where_patterns {
query.push_str(" ");
query.push_str(pattern);
query.push_str(" .\n");
}
for filter in &self.filters {
query.push_str(" FILTER (");
query.push_str(filter);
query.push_str(")\n");
}
query.push_str("}\n");
Ok(query)
}
}
impl SparqlQueryBuilder<Ask, Building> {
#[inline]
pub fn ask() -> Self {
Self {
query_type: PhantomData,
state: PhantomData,
prefixes: Vec::new(),
vars: Vec::new(),
construct_patterns: Vec::new(),
where_patterns: Vec::new(),
filters: Vec::new(),
order_by: Vec::new(),
limit_value: None,
offset_value: None,
distinct: false,
}
}
#[inline]
pub fn where_pattern(mut self, pattern: impl AsRef<str>) -> SparqlQueryBuilder<Ask, WithWhere> {
self.where_patterns.push(pattern.as_ref().to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
}
impl SparqlQueryBuilder<Ask, WithWhere> {
#[inline]
pub fn where_pattern(mut self, pattern: impl AsRef<str>) -> Self {
self.where_patterns.push(pattern.as_ref().to_string());
self
}
#[inline]
pub fn filter(mut self, filter: impl AsRef<str>) -> Self {
self.filters.push(filter.as_ref().to_string());
self
}
#[inline]
pub fn build(self) -> Result<String> {
let mut query = String::new();
for (prefix, iri) in &self.prefixes {
query.push_str(&format!("PREFIX {}: <{}>\n", prefix, iri));
}
query.push_str("ASK {\n");
for pattern in &self.where_patterns {
query.push_str(" ");
query.push_str(pattern);
query.push_str(" .\n");
}
for filter in &self.filters {
query.push_str(" FILTER (");
query.push_str(filter);
query.push_str(")\n");
}
query.push_str("}\n");
Ok(query)
}
}
impl SparqlQueryBuilder<Describe, Building> {
#[inline]
pub fn describe() -> Self {
Self {
query_type: PhantomData,
state: PhantomData,
prefixes: Vec::new(),
vars: Vec::new(),
construct_patterns: Vec::new(),
where_patterns: Vec::new(),
filters: Vec::new(),
order_by: Vec::new(),
limit_value: None,
offset_value: None,
distinct: false,
}
}
#[inline]
pub fn resource(mut self, iri: Iri) -> SparqlQueryBuilder<Describe, Complete> {
self.vars.push(format!("<{}>", iri.as_str()));
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
#[inline]
pub fn var(mut self, var: Variable) -> SparqlQueryBuilder<Describe, Complete> {
self.vars.push(var.as_str());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
}
impl SparqlQueryBuilder<Describe, Complete> {
#[inline]
pub fn where_pattern(
mut self, pattern: impl AsRef<str>,
) -> SparqlQueryBuilder<Describe, WithWhere> {
self.where_patterns.push(pattern.as_ref().to_string());
SparqlQueryBuilder {
query_type: PhantomData,
state: PhantomData,
prefixes: self.prefixes,
vars: self.vars,
construct_patterns: self.construct_patterns,
where_patterns: self.where_patterns,
filters: self.filters,
order_by: self.order_by,
limit_value: self.limit_value,
offset_value: self.offset_value,
distinct: self.distinct,
}
}
#[inline]
pub fn build(self) -> Result<String> {
let mut query = String::new();
for (prefix, iri) in &self.prefixes {
query.push_str(&format!("PREFIX {}: <{}>\n", prefix, iri));
}
query.push_str("DESCRIBE ");
query.push_str(&self.vars.join(" "));
query.push('\n');
Ok(query)
}
}
impl SparqlQueryBuilder<Describe, WithWhere> {
#[inline]
pub fn where_pattern(mut self, pattern: impl AsRef<str>) -> Self {
self.where_patterns.push(pattern.as_ref().to_string());
self
}
#[inline]
pub fn build(self) -> Result<String> {
let mut query = String::new();
for (prefix, iri) in &self.prefixes {
query.push_str(&format!("PREFIX {}: <{}>\n", prefix, iri));
}
query.push_str("DESCRIBE ");
query.push_str(&self.vars.join(" "));
query.push('\n');
query.push_str("WHERE {\n");
for pattern in &self.where_patterns {
query.push_str(" ");
query.push_str(pattern);
query.push_str(" .\n");
}
query.push_str("}\n");
Ok(query)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iri_validation_rejects_injection() {
let result = Iri::new("<malicious>");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid IRI format"));
}
#[test]
fn test_iri_validation_accepts_valid_iri() {
let result = Iri::new("https://example.com/resource");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "https://example.com/resource");
}
#[test]
fn test_variable_validation_rejects_injection() {
let result = Variable::new("var; DROP TABLE users;");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid variable"));
}
#[test]
fn test_variable_validation_accepts_valid_name() {
let result = Variable::new("valid_var_123");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "?valid_var_123");
}
#[test]
fn test_variable_handles_leading_question_mark() {
let result = Variable::new("?myvar");
assert!(result.is_ok());
assert_eq!(result.unwrap().as_str(), "?myvar");
}
#[test]
fn test_literal_escapes_special_characters() {
let lit = Literal::new("Alice's \"quote\" \n newline");
assert!(lit.as_str().contains("Alice's"));
assert!(lit.as_str().contains("\\\"quote\\\""));
assert!(lit.as_str().contains("\\n"));
}
#[test]
fn test_select_query_basic() {
let query = SparqlQueryBuilder::select()
.var(Variable::new("subject").unwrap())
.var(Variable::new("object").unwrap())
.where_pattern("?subject <http://example.com/pred> ?object")
.build()
.unwrap();
assert!(query.contains("SELECT ?subject ?object"));
assert!(query.contains("WHERE {"));
assert!(query.contains("?subject <http://example.com/pred> ?object"));
}
#[test]
fn test_select_query_with_prefix() {
let query = SparqlQueryBuilder::select()
.prefix("ex", Iri::new("https://example.com/").unwrap())
.var(Variable::new("s").unwrap())
.where_pattern("?s a ex:Entity")
.build()
.unwrap();
assert!(query.contains("PREFIX ex: <https://example.com/>"));
assert!(query.contains("SELECT ?s"));
}
#[test]
fn test_select_query_with_filter() {
let query = SparqlQueryBuilder::select()
.var(Variable::new("s").unwrap())
.where_pattern("?s <http://example.com/name> ?name")
.filter("?name = \"Alice\"")
.build()
.unwrap();
assert!(query.contains("FILTER (?name = \"Alice\")"));
}
#[test]
fn test_select_query_with_limit_and_offset() {
let query = SparqlQueryBuilder::select()
.var(Variable::new("s").unwrap())
.where_pattern("?s ?p ?o")
.limit(100)
.offset(50)
.build()
.unwrap();
assert!(query.contains("LIMIT 100"));
assert!(query.contains("OFFSET 50"));
}
#[test]
fn test_select_query_distinct() {
let query = SparqlQueryBuilder::select()
.distinct()
.var(Variable::new("s").unwrap())
.where_pattern("?s ?p ?o")
.build()
.unwrap();
assert!(query.contains("SELECT DISTINCT"));
}
#[test]
fn test_select_all_vars() {
let query = SparqlQueryBuilder::select()
.all_vars()
.where_pattern("?s ?p ?o")
.build()
.unwrap();
assert!(query.contains("SELECT *"));
}
#[test]
fn test_construct_query() {
let query = SparqlQueryBuilder::construct()
.construct_pattern("?s <http://example.com/new> ?o")
.where_pattern("?s <http://example.com/old> ?o")
.build()
.unwrap();
assert!(query.contains("CONSTRUCT {"));
assert!(query.contains("?s <http://example.com/new> ?o"));
assert!(query.contains("WHERE {"));
assert!(query.contains("?s <http://example.com/old> ?o"));
}
#[test]
fn test_ask_query() {
let query = SparqlQueryBuilder::ask()
.where_pattern("?s <http://example.com/pred> ?o")
.build()
.unwrap();
assert!(query.contains("ASK {"));
assert!(query.contains("?s <http://example.com/pred> ?o"));
}
#[test]
fn test_describe_query_with_iri() {
let query = SparqlQueryBuilder::describe()
.resource(Iri::new("http://example.com/alice").unwrap())
.build()
.unwrap();
assert!(query.contains("DESCRIBE <http://example.com/alice>"));
}
#[test]
fn test_describe_query_with_var() {
let query = SparqlQueryBuilder::describe()
.var(Variable::new("s").unwrap())
.where_pattern("?s a <http://example.com/Person>")
.build()
.unwrap();
assert!(query.contains("DESCRIBE ?s"));
assert!(query.contains("WHERE {"));
}
#[test]
fn test_order_by() {
let query = SparqlQueryBuilder::select()
.var(Variable::new("s").unwrap())
.var(Variable::new("name").unwrap())
.where_pattern("?s <http://example.com/name> ?name")
.order_by(Variable::new("name").unwrap())
.build()
.unwrap();
assert!(query.contains("ORDER BY ?name"));
}
}