Skip to main content

oxihuman_export/
sparql_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! SPARQL query stub export.
6
7/// SPARQL query type.
8#[derive(Debug, Clone, PartialEq)]
9pub enum SparqlQueryType {
10    Select,
11    Construct,
12    Ask,
13    Describe,
14}
15
16impl SparqlQueryType {
17    /// Keyword for this query type.
18    pub fn keyword(&self) -> &'static str {
19        match self {
20            Self::Select => "SELECT",
21            Self::Construct => "CONSTRUCT",
22            Self::Ask => "ASK",
23            Self::Describe => "DESCRIBE",
24        }
25    }
26}
27
28/// A SPARQL prefix declaration.
29#[derive(Debug, Clone)]
30pub struct SparqlPrefix {
31    pub prefix: String,
32    pub iri: String,
33}
34
35/// A SPARQL query builder.
36#[derive(Debug, Clone)]
37pub struct SparqlQuery {
38    pub query_type: SparqlQueryType,
39    pub prefixes: Vec<SparqlPrefix>,
40    pub variables: Vec<String>,
41    pub where_patterns: Vec<String>,
42    pub limit: Option<usize>,
43    pub offset: Option<usize>,
44}
45
46impl SparqlQuery {
47    /// Create a new SELECT query.
48    pub fn select(variables: Vec<String>) -> Self {
49        Self {
50            query_type: SparqlQueryType::Select,
51            prefixes: Vec::new(),
52            variables,
53            where_patterns: Vec::new(),
54            limit: None,
55            offset: None,
56        }
57    }
58
59    /// Add a prefix.
60    pub fn add_prefix(&mut self, prefix: impl Into<String>, iri: impl Into<String>) {
61        self.prefixes.push(SparqlPrefix {
62            prefix: prefix.into(),
63            iri: iri.into(),
64        });
65    }
66
67    /// Add a WHERE clause triple pattern.
68    pub fn add_pattern(&mut self, pattern: impl Into<String>) {
69        self.where_patterns.push(pattern.into());
70    }
71
72    /// Number of WHERE patterns.
73    pub fn pattern_count(&self) -> usize {
74        self.where_patterns.len()
75    }
76}
77
78/// Render a SPARQL query to a string.
79pub fn render_sparql(query: &SparqlQuery) -> String {
80    let mut out = String::new();
81    for p in &query.prefixes {
82        out.push_str(&format!("PREFIX {}: <{}>\n", p.prefix, p.iri));
83    }
84    let vars: Vec<String> = query.variables.iter().map(|v| format!("?{v}")).collect();
85    out.push_str(&format!(
86        "\n{} {}\n",
87        query.query_type.keyword(),
88        vars.join(" ")
89    ));
90    out.push_str("WHERE {\n");
91    for pat in &query.where_patterns {
92        out.push_str(&format!("  {pat}\n"));
93    }
94    out.push('}');
95    if let Some(limit) = query.limit {
96        out.push_str(&format!("\nLIMIT {limit}"));
97    }
98    if let Some(offset) = query.offset {
99        out.push_str(&format!("\nOFFSET {offset}"));
100    }
101    out.push('\n');
102    out
103}
104
105/// Validate that the query has at least one variable (for SELECT) and one pattern.
106pub fn validate_query(query: &SparqlQuery) -> bool {
107    (query.query_type != SparqlQueryType::Select || !query.variables.is_empty())
108        && !query.where_patterns.is_empty()
109}
110
111/// Add a standard Schema.org prefix.
112pub fn add_schema_prefix(query: &mut SparqlQuery) {
113    query.add_prefix("schema", "https://schema.org/");
114}
115
116/// Add an RDF prefix.
117pub fn add_rdf_prefix(query: &mut SparqlQuery) {
118    query.add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn sample_query() -> SparqlQuery {
126        let mut q = SparqlQuery::select(vec!["s".into(), "p".into(), "o".into()]);
127        q.add_prefix("schema", "https://schema.org/");
128        q.add_pattern("?s ?p ?o .");
129        q.limit = Some(10);
130        q
131    }
132
133    #[test]
134    fn pattern_count() {
135        assert_eq!(sample_query().pattern_count(), 1);
136    }
137
138    #[test]
139    fn render_contains_select() {
140        assert!(render_sparql(&sample_query()).contains("SELECT"));
141    }
142
143    #[test]
144    fn render_contains_where() {
145        assert!(render_sparql(&sample_query()).contains("WHERE"));
146    }
147
148    #[test]
149    fn render_contains_limit() {
150        assert!(render_sparql(&sample_query()).contains("LIMIT 10"));
151    }
152
153    #[test]
154    fn render_contains_prefix() {
155        assert!(render_sparql(&sample_query()).contains("PREFIX"));
156    }
157
158    #[test]
159    fn validate_ok() {
160        assert!(validate_query(&sample_query()));
161    }
162
163    #[test]
164    fn validate_no_pattern() {
165        let q = SparqlQuery::select(vec!["x".into()]);
166        assert!(!validate_query(&q));
167    }
168
169    #[test]
170    fn query_type_keyword() {
171        assert_eq!(SparqlQueryType::Ask.keyword(), "ASK");
172    }
173
174    #[test]
175    fn add_schema_prefix_adds_one() {
176        let mut q = SparqlQuery::select(vec![]);
177        add_schema_prefix(&mut q);
178        assert_eq!(q.prefixes.len(), 1);
179    }
180
181    #[test]
182    fn add_rdf_prefix_works() {
183        let mut q = SparqlQuery::select(vec![]);
184        add_rdf_prefix(&mut q);
185        assert!(q.prefixes[0].prefix == "rdf");
186    }
187}