| Crates.io | oxirs-gql |
| lib.rs | oxirs-gql |
| version | 0.1.0 |
| created_at | 2025-09-30 09:35:35.056532+00 |
| updated_at | 2026-01-20 21:38:29.426676+00 |
| description | GraphQL faΓ§ade for OxiRS with automatic schema generation from RDF ontologies |
| homepage | https://github.com/cool-japan/oxirs |
| repository | https://github.com/cool-japan/oxirs |
| max_upload_size | |
| id | 1860890 |
| size | 3,556,744 |
High-performance GraphQL server for RDF data with automatic schema generation
Status: Production Release (v0.1.0) - Released January 7, 2026
β¨ Production Release: Production-ready with API stability guarantees. Semantic versioning enforced.
oxirs-gql provides a GraphQL interface to RDF datasets, automatically generating GraphQL schemas from RDF vocabularies and enabling intuitive querying of semantic data. Built on top of Juniper, it offers seamless integration between GraphQL and SPARQL worlds.
Add to your Cargo.toml:
[dependencies]
oxirs-gql = "0.1.0"
use oxirs_gql::{Server, Schema, Config};
use oxirs_core::Dataset;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load RDF dataset
let dataset = Dataset::from_file("data.ttl")?;
// Generate GraphQL schema automatically
let schema = Schema::from_dataset(&dataset)?;
// Create server
let config = Config::builder()
.port(4000)
.enable_subscriptions(true)
.enable_introspection(true)
.build();
let server = Server::new(schema, config);
server.run().await
}
use oxirs_gql::{Schema, SchemaBuilder, TypeMapping};
use oxirs_core::{Dataset, NamedNode};
let dataset = Dataset::from_file("schema.ttl")?;
let schema = SchemaBuilder::new()
.dataset(dataset)
// Map RDF classes to GraphQL types
.map_class(
NamedNode::new("http://xmlns.com/foaf/0.1/Person")?,
TypeMapping::object("Person")
.field("name", "foaf:name")
.field("email", "foaf:mbox")
.field("friends", "foaf:knows")
)
// Custom resolvers
.resolver("Person", "fullName", |person| {
format!("{} {}", person.first_name?, person.last_name?)
})
.build()?;
use oxirs_gql::Schema;
use oxirs_core::Dataset;
// Load FOAF vocabulary
let dataset = Dataset::from_file("foaf.rdf")?;
// Generate schema automatically
let schema = Schema::from_dataset(&dataset)?;
// Generated GraphQL schema:
// type Person {
// name: String
// mbox: String
// knows: [Person!]!
// age: Int
// }
//
// type Query {
// person(id: ID!): Person
// persons: [Person!]!
// }
use oxirs_gql::{SchemaBuilder, TypeMapping, FieldMapping};
let schema = SchemaBuilder::new()
.map_class("foaf:Person", TypeMapping::object("Person")
.description("A person in the FOAF vocabulary")
.field("id", FieldMapping::id("@id"))
.field("name", FieldMapping::string("foaf:name")
.required(true))
.field("email", FieldMapping::string("foaf:mbox")
.transform(|email| email.strip_prefix("mailto:")))
.field("friends", FieldMapping::list("foaf:knows")
.item_type("Person"))
)
.build()?;
query GetPerson {
person(id: "http://example.org/alice") {
name
email
friends {
name
email
}
}
}
query SearchPeople($name: String!, $limit: Int = 10) {
people(filter: {name: {contains: $name}}, limit: $limit) {
nodes {
id
name
email
friendCount
}
pageInfo {
hasNextPage
endCursor
}
}
}
subscription NewPerson {
personAdded {
id
name
email
}
}
GraphQL queries are automatically translated to optimized SPARQL:
# GraphQL
query {
person(id: "http://example.org/alice") {
name
friends {
name
}
}
}
# Generated SPARQL
SELECT ?name ?friend_name WHERE {
<http://example.org/alice> foaf:name ?name .
OPTIONAL {
<http://example.org/alice> foaf:knows ?friend .
?friend foaf:name ?friend_name .
}
}
use oxirs_gql::{Resolver, Context};
#[derive(GraphQLObject)]
struct Person {
id: String,
name: String,
}
impl Person {
// Custom resolver with SPARQL
async fn friends_in_city(&self, ctx: &Context, city: String) -> Vec<Person> {
let query = format!(r#"
SELECT ?friend ?friend_name WHERE {{
<{}> foaf:knows ?friend .
?friend foaf:name ?friend_name .
?friend ex:livesIn ?city .
?city rdfs:label "{}" .
}}
"#, self.id, city);
ctx.execute_sparql(query).await?
.into_iter()
.map(|row| Person {
id: row.get("friend").unwrap().to_string(),
name: row.get("friend_name").unwrap().to_string(),
})
.collect()
}
}
use oxirs_gql::{FederatedSchema, RemoteSchema};
let federated = FederatedSchema::new()
.schema("users", RemoteSchema::new("http://users.example.com/graphql"))
.schema("products", RemoteSchema::new("http://products.example.com/graphql"))
.extend_type("User", |user| {
user.field("orders", "products.orders", |args| {
args.where_field("userId", user.id)
})
})
.build()?;
use oxirs_gql::{CustomScalar, ScalarValue};
use oxirs_core::NamedNode;
#[derive(GraphQLScalar)]
struct IRI(NamedNode);
impl CustomScalar for IRI {
fn serialize(&self) -> ScalarValue {
ScalarValue::String(self.0.to_string())
}
fn parse_value(value: &ScalarValue) -> Result<Self, String> {
match value {
ScalarValue::String(s) => {
NamedNode::new(s)
.map(IRI)
.map_err(|e| format!("Invalid IRI: {}", e))
}
_ => Err("IRI must be a string".to_string())
}
}
}
use oxirs_gql::{DataLoader, BatchFn};
use oxirs_core::{Dataset, NamedNode};
struct PersonLoader {
dataset: Dataset,
}
impl BatchFn<String, Person> for PersonLoader {
async fn load(&self, keys: &[String]) -> Vec<Person> {
let query = format!(r#"
SELECT ?id ?name WHERE {{
VALUES ?id {{ {} }}
?id foaf:name ?name .
}}
"#, keys.iter().map(|k| format!("<{}>", k)).collect::<Vec<_>>().join(" "));
self.dataset.query(&query).await
.unwrap()
.into_iter()
.map(|row| Person {
id: row.get("id").unwrap().to_string(),
name: row.get("name").unwrap().to_string(),
})
.collect()
}
}
// Usage in resolver
async fn friends(&self, ctx: &Context) -> Vec<Person> {
let loader: DataLoader<PersonLoader> = ctx.data()?;
let friend_ids = self.get_friend_ids();
loader.load_many(friend_ids).await
}
server:
host: "0.0.0.0"
port: 4000
cors: true
playground: true
introspection: true
schema:
auto_generate: true
vocabularies:
- "http://xmlns.com/foaf/0.1/"
- "http://schema.org/"
mapping:
naming_convention: "camelCase"
max_depth: 10
enable_filters: true
enable_pagination: true
performance:
query_cache: true
cache_size: 1000
max_query_depth: 15
max_query_complexity: 1000
subscriptions:
enabled: true
transport: "websocket"
keep_alive: 30
use oxirs_gql::{Config, NamingConvention, CacheConfig};
let config = Config::builder()
.auto_generate_schema(true)
.naming_convention(NamingConvention::CamelCase)
.max_query_depth(15)
.enable_introspection(true)
.cache(CacheConfig::new()
.query_cache(true)
.schema_cache(true)
.ttl(Duration::from_secs(300)))
.build();
| Operation | QPS | Latency (p95) | Memory |
|---|---|---|---|
| Simple query | 12,000 | 15ms | 32MB |
| Complex nested | 3,500 | 45ms | 45MB |
| Subscription | 8,000 | 8ms | 28MB |
| Schema introspection | 15,000 | 5ms | 25MB |
use oxirs_gql::{QueryOptimizer, CachingStrategy};
let optimizer = QueryOptimizer::new()
.enable_query_planning(true)
.enable_result_caching(true)
.caching_strategy(CachingStrategy::LRU { size: 1000 })
.sparql_optimization(true);
let schema = Schema::new(dataset)
.optimizer(optimizer)
.build()?;
FROM rust:1.84 as builder
WORKDIR /app
COPY . .
RUN cargo build --release --bin oxirs-gql
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/oxirs-gql /usr/local/bin/
EXPOSE 4000
CMD ["oxirs-gql", "--config", "/config.yaml"]
apiVersion: apps/v1
kind: Deployment
metadata:
name: oxirs-gql
spec:
replicas: 3
selector:
matchLabels:
app: oxirs-gql
template:
spec:
containers:
- name: oxirs-gql
image: ghcr.io/cool-japan/oxirs-gql:latest
ports:
- containerPort: 4000
env:
- name: GRAPHQL_PLAYGROUND
value: "false"
- name: GRAPHQL_INTROSPECTION
value: "false"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
oxirs-core: RDF data modeloxirs-fuseki: SPARQL HTTP serveroxirs-arq: SPARQL query engineoxirs-stream: Real-time subscriptionsLicensed under either of:
at your option.
π Production Release (v0.1.0) - January 7, 2026
Current features:
APIs follow semantic versioning. See CHANGELOG.md for details.