sparql_http_client
An async, typed SPARQL HTTP client for Rust with optional compile-time query validation.
Quick start
use futures_util::StreamExt;
use sparql_http_client::{Endpoint, SparqlClient, query};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let endpoint = Endpoint::new(
SparqlClient::default(),
"https://query.wikidata.org/bigdata/namespace/wdq/sparql",
);
let response = query!(endpoint, r#"
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?countryLabel ?population WHERE {
?country wdt:P31 wd:Q3624078 ;
wdt:P1082 ?population ;
rdfs:label ?countryLabel .
FILTER(LANG(?countryLabel) = "en")
}
ORDER BY DESC(?population)
LIMIT 5
"#)
.run()
.await?;
let mut rows = std::pin::pin!(response.into_rows());
while let Some(row) = rows.next().await {
let row = row?;
let label = row.get("countryLabel").map(|t| t.value.as_ref()).unwrap_or("?");
let pop = row.get("population").map(|t| t.value.as_ref()).unwrap_or("?");
println!("{label}: {pop}");
}
Ok(())
}
Compile-time query validation
The query! macro validates SPARQL syntax at compile time:
- Syntax errors are caught as compile errors, not runtime panics
- The query kind (
SELECT, ASK, …) is resolved at compile time, so the
return type is already SelectQuery or AskQuery — no runtime dispatch,
no Result to unwrap
let q = query!(endpoint, "SELCT ?s WHERE { ?s ?p ?o }");
For runtime validation, parse a string using str::parse:
use sparql_http_client::SelectQueryString;
let qs: Result<SelectQueryString, _> = "SELECT ?s WHERE { ?s ?p ?o }".parse();
assert!(qs.is_ok());
let qs: Result<SelectQueryString, _> = "not sparql".parse();
assert!(qs.is_err());
Query types
| Query kind |
String type |
Response type |
SELECT |
SelectQueryString |
SelectQueryResponse |
ASK |
AskQueryString |
AskQueryResponse |
ASK queries
use sparql_http_client::{Endpoint, SparqlClient, query};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let endpoint = Endpoint::new(
SparqlClient::default(),
"https://query.wikidata.org/bigdata/namespace/wdq/sparql",
);
let is_country: bool = query!(
endpoint,
"ASK { <http://www.wikidata.org/entity/Q142> \
<http://www.wikidata.org/prop/direct/P31> \
<http://www.wikidata.org/entity/Q3624078> }"
)
.run()
.await?
.into();
println!("Is a sovereign state: {is_country}");
Ok(())
}
Setting a User-Agent
Many public SPARQL endpoints ask callers to provide a meaningful User-Agent
so administrators can identify and contact heavy users:
use sparql_http_client::{Endpoint, SparqlClient, UserAgent};
let endpoint = Endpoint::new(
SparqlClient::new(UserAgent {
name: "my-app".into(),
version: "1.0.0".into(),
contact: "mailto:user@example.com".into(),
}),
"https://query.wikidata.org/bigdata/namespace/wdq/sparql",
);
Accessing response data
term.value always holds the string representation of an RDF term regardless
of its type, so reading values requires no type matching:
use futures_util::StreamExt;
use sparql_http_client::{Endpoint, SparqlClient, query};
# #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
# let endpoint = Endpoint::new(SparqlClient::default(), "https://example.org/sparql");
let response = query!(endpoint, "SELECT ?label WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label } LIMIT 5")
.run().await?;
let mut rows = std::pin::pin!(response.into_rows());
while let Some(row) = rows.next().await {
let row = row?;
if let Some(term) = row.get("label") {
println!("{}", term.value);
}
}
# Ok(()) }
When the RDF type matters, use the convenience methods on RDFTerm:
use futures_util::StreamExt;
use sparql_http_client::{Endpoint, SparqlClient, query};
# #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
# let endpoint = Endpoint::new(SparqlClient::default(), "https://example.org/sparql");
let response = query!(endpoint, "SELECT ?obj WHERE { ?s ?p ?obj } LIMIT 5")
.run().await?;
let mut rows = std::pin::pin!(response.into_rows());
while let Some(row) = rows.next().await {
let row = row?;
if let Some(term) = row.get("obj") {
if term.is_iri() {
println!("IRI: {}", term.value);
} else if let Some(lang) = term.lang() {
println!("Literal (lang={lang}): {}", term.value);
} else if let Some(dt) = term.datatype() {
println!("Literal (type={dt}): {}", term.value);
} else {
println!("Literal: {}", term.value);
}
}
}
# Ok(()) }
Or match on kind for exhaustive handling:
use futures_util::StreamExt;
use sparql_http_client::{Endpoint, SparqlClient, query};
use sparql_http_client::response::{RDFType, LiteralType};
# #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
# let endpoint = Endpoint::new(SparqlClient::default(), "https://example.org/sparql");
let response = query!(endpoint, "SELECT ?obj WHERE { ?s ?p ?obj } LIMIT 5")
.run().await?;
let mut rows = std::pin::pin!(response.into_rows());
while let Some(row) = rows.next().await {
let row = row?;
if let Some(term) = row.get("obj") {
match &term.kind {
RDFType::IRI => println!("IRI: {}", term.value),
RDFType::BlankNode => println!("blank node"),
RDFType::Literal(LiteralType::Lang(lang)) => {
println!("\"{}\"@{lang}", term.value);
}
RDFType::Literal(LiteralType::Datatype(dt)) => {
println!("\"{}\"^^{dt}", term.value);
}
RDFType::Literal(LiteralType::Plain) => {
println!("\"{}\"", term.value);
}
}
}
}
# Ok(()) }