use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TemplateParams {
params: HashMap<String, String>,
}
impl TemplateParams {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.params.insert(key.into(), value.into());
self
}
pub fn get(&self, key: &str) -> Option<&String> {
self.params.get(key)
}
pub fn has(&self, key: &str) -> bool {
self.params.contains_key(key)
}
pub fn keys(&self) -> Vec<&String> {
self.params.keys().collect()
}
pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.set(key, value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryTemplate {
pub name: String,
pub description: String,
pub template: String,
pub required_params: Vec<String>,
pub optional_params: HashMap<String, String>,
pub category: TemplateCategory,
pub example: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TemplateCategory {
Retrieval,
Modification,
Aggregation,
PatternMatching,
Search,
Admin,
Custom,
}
impl QueryTemplate {
pub fn new(name: impl Into<String>, template: impl Into<String>) -> Self {
Self {
name: name.into(),
description: String::new(),
template: template.into(),
required_params: Vec::new(),
optional_params: HashMap::new(),
category: TemplateCategory::Custom,
example: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_required(mut self, param: impl Into<String>) -> Self {
self.required_params.push(param.into());
self
}
pub fn with_optional(mut self, param: impl Into<String>, default: impl Into<String>) -> Self {
self.optional_params.insert(param.into(), default.into());
self
}
pub fn with_category(mut self, category: TemplateCategory) -> Self {
self.category = category;
self
}
pub fn with_example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
pub fn render(&self, params: &TemplateParams) -> Result<String> {
for required in &self.required_params {
if !params.has(required) {
return Err(anyhow!(
"Missing required parameter '{}' for template '{}'",
required,
self.name
));
}
}
let mut result = self.template.clone();
for (key, value) in ¶ms.params {
let placeholder = format!("{{{{{}}}}}", key);
result = result.replace(&placeholder, value);
}
for (key, default) in &self.optional_params {
let placeholder = format!("{{{{{}}}}}", key);
if result.contains(&placeholder) {
let value = params.get(key).unwrap_or(default);
result = result.replace(&placeholder, value);
}
}
if result.contains("{{") {
return Err(anyhow!(
"Template '{}' contains unsubstituted placeholders",
self.name
));
}
Ok(result)
}
}
pub struct TemplateRegistry {
templates: HashMap<String, QueryTemplate>,
}
impl TemplateRegistry {
pub fn new() -> Self {
Self {
templates: HashMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut registry = Self::new();
registry.register_default_templates();
registry
}
pub fn register(&mut self, template: QueryTemplate) {
self.templates.insert(template.name.clone(), template);
}
pub fn get(&self, name: &str) -> Option<&QueryTemplate> {
self.templates.get(name)
}
pub fn render(&self, name: &str, params: &TemplateParams) -> Result<String> {
let template = self
.get(name)
.ok_or_else(|| anyhow!("Template '{}' not found", name))?;
template.render(params)
}
pub fn list_templates(&self) -> Vec<&String> {
self.templates.keys().collect()
}
pub fn list_by_category(&self, category: TemplateCategory) -> Vec<&QueryTemplate> {
self.templates
.values()
.filter(|t| t.category == category)
.collect()
}
fn register_default_templates(&mut self) {
self.register(
QueryTemplate::new(
"find_by_property",
"SELECT ?subject WHERE {\n ?subject {{predicate}} {{value}} .\n} LIMIT {{limit}}",
)
.with_description("Find resources by property value")
.with_required("predicate")
.with_required("value")
.with_optional("limit", "100")
.with_category(TemplateCategory::Retrieval)
.with_example("Find all people named Alice"),
);
self.register(
QueryTemplate::new(
"get_all_properties",
"SELECT ?property ?value WHERE {\n {{subject}} ?property ?value .\n} LIMIT {{limit}}"
)
.with_description("Get all properties of a specific resource")
.with_required("subject")
.with_optional("limit", "100")
.with_category(TemplateCategory::Retrieval)
);
self.register(
QueryTemplate::new(
"count_by_type",
"SELECT (COUNT(?subject) AS ?count) WHERE {\n ?subject a {{type}} .\n}",
)
.with_description("Count resources of a specific type")
.with_required("type")
.with_category(TemplateCategory::Aggregation),
);
self.register(
QueryTemplate::new(
"search_by_label",
"SELECT ?subject ?label WHERE {\n ?subject rdfs:label ?label .\n FILTER(CONTAINS(LCASE(?label), LCASE({{search_term}})))\n} LIMIT {{limit}}"
)
.with_description("Search resources by label (case-insensitive)")
.with_required("search_term")
.with_optional("limit", "50")
.with_category(TemplateCategory::Search)
);
self.register(
QueryTemplate::new(
"insert_triple",
"INSERT DATA {\n {{subject}} {{predicate}} {{object}} .\n}",
)
.with_description("Insert a single triple")
.with_required("subject")
.with_required("predicate")
.with_required("object")
.with_category(TemplateCategory::Modification),
);
self.register(
QueryTemplate::new(
"delete_by_pattern",
"DELETE WHERE {\n ?subject {{predicate}} {{value}} .\n}",
)
.with_description("Delete triples matching a pattern")
.with_required("predicate")
.with_required("value")
.with_category(TemplateCategory::Modification),
);
self.register(
QueryTemplate::new(
"distinct_values",
"SELECT DISTINCT ?value WHERE {\n ?subject {{predicate}} ?value .\n} ORDER BY ?value LIMIT {{limit}}"
)
.with_description("Get distinct values for a property")
.with_required("predicate")
.with_optional("limit", "100")
.with_category(TemplateCategory::Retrieval)
);
self.register(
QueryTemplate::new(
"group_count",
"SELECT ?{{group_var}} (COUNT(*) AS ?count) WHERE {\n {{pattern}}\n} GROUP BY ?{{group_var}} ORDER BY DESC(?count) LIMIT {{limit}}"
)
.with_description("Count occurrences grouped by a variable")
.with_required("group_var")
.with_required("pattern")
.with_optional("limit", "50")
.with_category(TemplateCategory::Aggregation)
);
self.register(
QueryTemplate::new(
"property_path",
"SELECT ?target WHERE {\n {{source}} {{path}} ?target .\n} LIMIT {{limit}}",
)
.with_description("Query using property paths (e.g., rdfs:subClassOf*)")
.with_required("source")
.with_required("path")
.with_optional("limit", "100")
.with_category(TemplateCategory::PatternMatching),
);
self.register(
QueryTemplate::new(
"with_optional",
"SELECT ?subject ?required ?optional WHERE {\n ?subject {{required_predicate}} ?required .\n OPTIONAL { ?subject {{optional_predicate}} ?optional . }\n} LIMIT {{limit}}"
)
.with_description("Query with required and optional patterns")
.with_required("required_predicate")
.with_required("optional_predicate")
.with_optional("limit", "100")
.with_category(TemplateCategory::PatternMatching)
);
}
}
impl Default for TemplateRegistry {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_params() {
let mut params = TemplateParams::new();
params.set("key1", "value1");
params.set("key2", "value2");
assert_eq!(params.get("key1"), Some(&"value1".to_string()));
assert_eq!(params.get("key2"), Some(&"value2".to_string()));
assert!(params.has("key1"));
assert!(!params.has("key3"));
}
#[test]
fn test_template_params_builder() {
let params = TemplateParams::new()
.with("name", "Alice")
.with("age", "30");
assert_eq!(params.get("name"), Some(&"Alice".to_string()));
assert_eq!(params.get("age"), Some(&"30".to_string()));
}
#[test]
fn test_simple_template_render() {
let template = QueryTemplate::new("test", "SELECT * WHERE { ?s {{predicate}} {{object}} }")
.with_required("predicate")
.with_required("object");
let params = TemplateParams::new()
.with("predicate", "foaf:name")
.with("object", "\"Alice\"");
let result = template.render(¶ms).unwrap();
assert!(result.contains("foaf:name"));
assert!(result.contains("\"Alice\""));
}
#[test]
fn test_missing_required_param() {
let template = QueryTemplate::new("test", "SELECT * WHERE { ?s {{predicate}} ?o }")
.with_required("predicate");
let params = TemplateParams::new();
let result = template.render(¶ms);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required parameter"));
}
#[test]
fn test_optional_params() {
let template = QueryTemplate::new("test", "SELECT * WHERE { ?s ?p ?o } LIMIT {{limit}}")
.with_optional("limit", "100");
let params1 = TemplateParams::new();
let result1 = template.render(¶ms1).unwrap();
assert!(result1.contains("LIMIT 100"));
let params2 = TemplateParams::new().with("limit", "50");
let result2 = template.render(¶ms2).unwrap();
assert!(result2.contains("LIMIT 50"));
}
#[test]
fn test_registry_register_and_get() {
let mut registry = TemplateRegistry::new();
let template = QueryTemplate::new("test", "SELECT * WHERE { ?s ?p ?o }");
registry.register(template);
assert!(registry.get("test").is_some());
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_registry_render() {
let mut registry = TemplateRegistry::new();
registry.register(
QueryTemplate::new("test", "SELECT * WHERE { ?s {{pred}} ?o }").with_required("pred"),
);
let params = TemplateParams::new().with("pred", "foaf:name");
let result = registry.render("test", ¶ms).unwrap();
assert!(result.contains("foaf:name"));
}
#[test]
fn test_default_templates() {
let registry = TemplateRegistry::with_defaults();
assert!(registry.get("find_by_property").is_some());
assert!(registry.get("get_all_properties").is_some());
assert!(registry.get("count_by_type").is_some());
assert!(registry.get("search_by_label").is_some());
}
#[test]
fn test_find_by_property_template() {
let registry = TemplateRegistry::with_defaults();
let params = TemplateParams::new()
.with("predicate", "foaf:name")
.with("value", "\"Alice\"");
let query = registry.render("find_by_property", ¶ms).unwrap();
assert!(query.contains("foaf:name"));
assert!(query.contains("\"Alice\""));
assert!(query.contains("LIMIT 100")); }
#[test]
fn test_count_by_type_template() {
let registry = TemplateRegistry::with_defaults();
let params = TemplateParams::new().with("type", "foaf:Person");
let query = registry.render("count_by_type", ¶ms).unwrap();
assert!(query.contains("COUNT"));
assert!(query.contains("foaf:Person"));
}
#[test]
fn test_insert_triple_template() {
let registry = TemplateRegistry::with_defaults();
let params = TemplateParams::new()
.with("subject", "<http://example.org/alice>")
.with("predicate", "foaf:name")
.with("object", "\"Alice\"");
let query = registry.render("insert_triple", ¶ms).unwrap();
assert!(query.contains("INSERT DATA"));
assert!(query.contains("<http://example.org/alice>"));
assert!(query.contains("foaf:name"));
assert!(query.contains("\"Alice\""));
}
#[test]
fn test_list_templates() {
let registry = TemplateRegistry::with_defaults();
let templates = registry.list_templates();
assert!(!templates.is_empty());
assert!(templates.contains(&&"find_by_property".to_string()));
}
#[test]
fn test_list_by_category() {
let registry = TemplateRegistry::with_defaults();
let retrieval = registry.list_by_category(TemplateCategory::Retrieval);
let aggregation = registry.list_by_category(TemplateCategory::Aggregation);
assert!(!retrieval.is_empty());
assert!(!aggregation.is_empty());
for template in retrieval {
assert_eq!(template.category, TemplateCategory::Retrieval);
}
}
#[test]
fn test_search_by_label_template() {
let registry = TemplateRegistry::with_defaults();
let params = TemplateParams::new().with("search_term", "\"person\"");
let query = registry.render("search_by_label", ¶ms).unwrap();
assert!(query.contains("rdfs:label"));
assert!(query.contains("FILTER"));
assert!(query.contains("CONTAINS"));
}
#[test]
fn test_template_with_custom_limit() {
let registry = TemplateRegistry::with_defaults();
let params = TemplateParams::new()
.with("predicate", "foaf:name")
.with("value", "\"Alice\"")
.with("limit", "500");
let query = registry.render("find_by_property", ¶ms).unwrap();
assert!(query.contains("LIMIT 500"));
}
}