Skip to main content

oxirs_arq/
update_graph_management_protocol.rs

1//! SPARQL 1.1 UPDATE Graph Management — Protocol and Request Parsing
2//!
3//! HTTP/SPARQL protocol layer: parses raw SPARQL Update graph management
4//! request strings into [`GraphManagementOp`] values, and serialises
5//! [`GraphManagementResult`] into HTTP-compatible response structures.
6
7use thiserror::Error;
8
9use crate::update_graph_management_types::{
10    GraphManagementOp, GraphManagementResult, GraphManagementTarget,
11};
12
13// ---------------------------------------------------------------------------
14// Errors
15// ---------------------------------------------------------------------------
16
17/// Errors produced by the graph-management protocol layer.
18#[derive(Debug, Error)]
19pub enum GraphManagementProtocolError {
20    /// The SPARQL Update string could not be parsed as a graph management op.
21    #[error("parse error: {0}")]
22    Parse(String),
23
24    /// A required IRI argument was missing or malformed.
25    #[error("invalid IRI: {0}")]
26    InvalidIri(String),
27}
28
29// ---------------------------------------------------------------------------
30// HTTP response representation
31// ---------------------------------------------------------------------------
32
33/// HTTP-level response from a graph management update request.
34#[derive(Debug, Clone)]
35pub struct GraphManagementHttpResponse {
36    /// HTTP status code (200 OK or 4xx/5xx on failure).
37    pub status_code: u16,
38    /// Human-readable message body.
39    pub body: String,
40    /// Structured result (if the operation succeeded).
41    pub result: Option<GraphManagementResult>,
42}
43
44impl GraphManagementHttpResponse {
45    /// Build a success response.
46    pub fn ok(result: GraphManagementResult) -> Self {
47        let body = format!(
48            "OK: {} triples affected, {} graphs affected",
49            result.triples_affected,
50            result.graphs_affected.len()
51        );
52        Self {
53            status_code: 200,
54            body,
55            result: Some(result),
56        }
57    }
58
59    /// Build a failure response.
60    pub fn error(status_code: u16, message: impl Into<String>) -> Self {
61        Self {
62            status_code,
63            body: message.into(),
64            result: None,
65        }
66    }
67
68    /// Returns `true` if the HTTP status indicates success (2xx).
69    pub fn is_success(&self) -> bool {
70        self.status_code >= 200 && self.status_code < 300
71    }
72}
73
74// ---------------------------------------------------------------------------
75// Parser
76// ---------------------------------------------------------------------------
77
78/// Minimal parser for SPARQL 1.1 graph management update strings.
79///
80/// Supports the following subset of the SPARQL 1.1 Update grammar:
81///
82/// ```text
83/// GraphManagement  ::=  Load | Clear | Drop | Create | Copy | Move | Add
84/// Load             ::=  'LOAD' 'SILENT'? IRIref ( 'INTO' 'GRAPH' IRIref )?
85/// Clear            ::=  'CLEAR' 'SILENT'? GraphRefAll
86/// Drop             ::=  'DROP' 'SILENT'? GraphRefAll
87/// Create           ::=  'CREATE' 'SILENT'? 'GRAPH' IRIref
88/// Copy             ::=  'COPY' 'SILENT'? GraphOrDefault 'TO' GraphOrDefault
89/// Move             ::=  'MOVE' 'SILENT'? GraphOrDefault 'TO' GraphOrDefault
90/// Add              ::=  'ADD' 'SILENT'? GraphOrDefault 'TO' GraphOrDefault
91/// GraphRefAll      ::=  'DEFAULT' | 'NAMED' | 'ALL' | 'GRAPH' IRIref
92/// GraphOrDefault   ::=  'DEFAULT' | 'GRAPH'? IRIref
93/// ```
94pub struct GraphManagementParser;
95
96impl GraphManagementParser {
97    /// Parse a SPARQL 1.1 UPDATE graph management statement.
98    ///
99    /// Input tokens are compared case-insensitively.  IRIs must be enclosed in
100    /// angle brackets (`<iri>`).
101    ///
102    /// # Errors
103    ///
104    /// Returns [`GraphManagementProtocolError::Parse`] when the input does not
105    /// match any known graph management operation.
106    pub fn parse(input: &str) -> Result<GraphManagementOp, GraphManagementProtocolError> {
107        let tokens: Vec<&str> = input.split_whitespace().collect();
108        if tokens.is_empty() {
109            return Err(GraphManagementProtocolError::Parse(
110                "empty input".to_string(),
111            ));
112        }
113
114        let keyword = tokens[0].to_uppercase();
115        match keyword.as_str() {
116            "LOAD" => Self::parse_load(&tokens[1..]),
117            "CLEAR" => Self::parse_clear(&tokens[1..]),
118            "DROP" => Self::parse_drop(&tokens[1..]),
119            "CREATE" => Self::parse_create(&tokens[1..]),
120            "COPY" => Self::parse_copy(&tokens[1..]),
121            "MOVE" => Self::parse_move(&tokens[1..]),
122            "ADD" => Self::parse_add(&tokens[1..]),
123            other => Err(GraphManagementProtocolError::Parse(format!(
124                "unknown graph management keyword: {other}"
125            ))),
126        }
127    }
128
129    // -----------------------------------------------------------------------
130    // Operation-specific parsers
131    // -----------------------------------------------------------------------
132
133    fn parse_load(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
134        let mut pos = 0;
135        let silent = Self::consume_silent(tokens, &mut pos);
136
137        let iri = Self::consume_iri(tokens, &mut pos)?;
138
139        let into_graph = if pos < tokens.len()
140            && tokens[pos].to_uppercase() == "INTO"
141            && pos + 1 < tokens.len()
142            && tokens[pos + 1].to_uppercase() == "GRAPH"
143        {
144            pos += 2; // consume INTO GRAPH
145            Some(Self::consume_iri(tokens, &mut pos)?)
146        } else {
147            None
148        };
149
150        Ok(GraphManagementOp::Load {
151            iri,
152            into_graph,
153            silent,
154        })
155    }
156
157    fn parse_clear(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
158        let mut pos = 0;
159        let silent = Self::consume_silent(tokens, &mut pos);
160        let target = Self::consume_graph_ref_all(tokens, &mut pos)?;
161        Ok(GraphManagementOp::Clear { target, silent })
162    }
163
164    fn parse_drop(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
165        let mut pos = 0;
166        let silent = Self::consume_silent(tokens, &mut pos);
167        let target = Self::consume_graph_ref_all(tokens, &mut pos)?;
168        Ok(GraphManagementOp::Drop { target, silent })
169    }
170
171    fn parse_create(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
172        let mut pos = 0;
173        let silent = Self::consume_silent(tokens, &mut pos);
174
175        if pos < tokens.len() && tokens[pos].to_uppercase() == "GRAPH" {
176            pos += 1;
177        }
178
179        let graph = Self::consume_iri(tokens, &mut pos)?;
180        Ok(GraphManagementOp::Create { graph, silent })
181    }
182
183    fn parse_copy(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
184        let mut pos = 0;
185        let silent = Self::consume_silent(tokens, &mut pos);
186        let source = Self::consume_graph_or_default(tokens, &mut pos)?;
187
188        if pos >= tokens.len() || tokens[pos].to_uppercase() != "TO" {
189            return Err(GraphManagementProtocolError::Parse(
190                "expected TO after source graph in COPY".to_string(),
191            ));
192        }
193        pos += 1;
194
195        let destination = Self::consume_graph_or_default(tokens, &mut pos)?;
196        Ok(GraphManagementOp::Copy {
197            source,
198            destination,
199            silent,
200        })
201    }
202
203    fn parse_move(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
204        let mut pos = 0;
205        let silent = Self::consume_silent(tokens, &mut pos);
206        let source = Self::consume_graph_or_default(tokens, &mut pos)?;
207
208        if pos >= tokens.len() || tokens[pos].to_uppercase() != "TO" {
209            return Err(GraphManagementProtocolError::Parse(
210                "expected TO after source graph in MOVE".to_string(),
211            ));
212        }
213        pos += 1;
214
215        let destination = Self::consume_graph_or_default(tokens, &mut pos)?;
216        Ok(GraphManagementOp::Move {
217            source,
218            destination,
219            silent,
220        })
221    }
222
223    fn parse_add(tokens: &[&str]) -> Result<GraphManagementOp, GraphManagementProtocolError> {
224        let mut pos = 0;
225        let silent = Self::consume_silent(tokens, &mut pos);
226        let source = Self::consume_graph_or_default(tokens, &mut pos)?;
227
228        if pos >= tokens.len() || tokens[pos].to_uppercase() != "TO" {
229            return Err(GraphManagementProtocolError::Parse(
230                "expected TO after source graph in ADD".to_string(),
231            ));
232        }
233        pos += 1;
234
235        let destination = Self::consume_graph_or_default(tokens, &mut pos)?;
236        Ok(GraphManagementOp::Add {
237            source,
238            destination,
239            silent,
240        })
241    }
242
243    // -----------------------------------------------------------------------
244    // Token helpers
245    // -----------------------------------------------------------------------
246
247    /// Consume the optional `SILENT` keyword and advance `pos`.
248    fn consume_silent(tokens: &[&str], pos: &mut usize) -> bool {
249        if *pos < tokens.len() && tokens[*pos].to_uppercase() == "SILENT" {
250            *pos += 1;
251            true
252        } else {
253            false
254        }
255    }
256
257    /// Consume an `<iri>` token (must start with `<` and end with `>`).
258    fn consume_iri(
259        tokens: &[&str],
260        pos: &mut usize,
261    ) -> Result<String, GraphManagementProtocolError> {
262        if *pos >= tokens.len() {
263            return Err(GraphManagementProtocolError::Parse(
264                "expected IRI but found end of input".to_string(),
265            ));
266        }
267
268        let token = tokens[*pos];
269        if token.starts_with('<') && token.ends_with('>') && token.len() >= 2 {
270            *pos += 1;
271            Ok(token[1..token.len() - 1].to_owned())
272        } else {
273            Err(GraphManagementProtocolError::InvalidIri(format!(
274                "expected <iri>, got: {token}"
275            )))
276        }
277    }
278
279    /// Parse a `GraphRefAll` production: `DEFAULT | NAMED | ALL | GRAPH <iri>`.
280    fn consume_graph_ref_all(
281        tokens: &[&str],
282        pos: &mut usize,
283    ) -> Result<GraphManagementTarget, GraphManagementProtocolError> {
284        if *pos >= tokens.len() {
285            return Err(GraphManagementProtocolError::Parse(
286                "expected graph reference but found end of input".to_string(),
287            ));
288        }
289
290        match tokens[*pos].to_uppercase().as_str() {
291            "DEFAULT" => {
292                *pos += 1;
293                Ok(GraphManagementTarget::Default)
294            }
295            "NAMED" => {
296                *pos += 1;
297                Ok(GraphManagementTarget::AllNamed)
298            }
299            "ALL" => {
300                *pos += 1;
301                Ok(GraphManagementTarget::All)
302            }
303            "GRAPH" => {
304                *pos += 1;
305                let iri = Self::consume_iri(tokens, pos)?;
306                Ok(GraphManagementTarget::Named(iri))
307            }
308            other => Err(GraphManagementProtocolError::Parse(format!(
309                "expected DEFAULT | NAMED | ALL | GRAPH <iri>, got: {other}"
310            ))),
311        }
312    }
313
314    /// Parse a `GraphOrDefault` production: `DEFAULT | GRAPH? <iri>`.
315    fn consume_graph_or_default(
316        tokens: &[&str],
317        pos: &mut usize,
318    ) -> Result<GraphManagementTarget, GraphManagementProtocolError> {
319        if *pos >= tokens.len() {
320            return Err(GraphManagementProtocolError::Parse(
321                "expected graph or DEFAULT but found end of input".to_string(),
322            ));
323        }
324
325        match tokens[*pos].to_uppercase().as_str() {
326            "DEFAULT" => {
327                *pos += 1;
328                Ok(GraphManagementTarget::Default)
329            }
330            "GRAPH" => {
331                *pos += 1;
332                let iri = Self::consume_iri(tokens, pos)?;
333                Ok(GraphManagementTarget::Named(iri))
334            }
335            _ => {
336                // Bare IRI (the GRAPH keyword is optional in GraphOrDefault)
337                let iri = Self::consume_iri(tokens, pos)?;
338                Ok(GraphManagementTarget::Named(iri))
339            }
340        }
341    }
342}
343
344// ---------------------------------------------------------------------------
345// Dispatcher
346// ---------------------------------------------------------------------------
347
348/// Combines [`GraphManagementParser`] with
349/// [`GraphManagementExecutor`](crate::update_graph_management_ops::GraphManagementExecutor)
350/// into a single HTTP-level request handler.
351pub struct GraphManagementRequestHandler;
352
353impl GraphManagementRequestHandler {
354    /// Parse and execute a SPARQL Update graph management statement.
355    ///
356    /// On success returns a 200 response; on parse error returns 400; on
357    /// runtime error returns 500.
358    pub fn handle(
359        input: &str,
360        dataset: &mut crate::update_graph_management_types::GraphManagementDataset,
361    ) -> GraphManagementHttpResponse {
362        let op = match GraphManagementParser::parse(input) {
363            Ok(op) => op,
364            Err(e) => {
365                return GraphManagementHttpResponse::error(400, format!("Bad Request: {e}"));
366            }
367        };
368
369        match crate::update_graph_management_ops::GraphManagementExecutor::execute(&op, dataset) {
370            Ok(result) => GraphManagementHttpResponse::ok(result),
371            Err(e) => {
372                GraphManagementHttpResponse::error(500, format!("Internal Server Error: {e}"))
373            }
374        }
375    }
376}