1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// Copyright (c) 2018-2023, agnos.ai UK Ltd, all rights reserved.
//---------------------------------------------------------------

use {
    crate::{Cursor, DataStoreConnection, Namespaces, Parameters},
    core::fmt::{Display, Formatter},
    indoc::formatdoc,
    rdf_store_rs::{
        consts::{DEFAULT_GRAPH_RDFOX, LOG_TARGET_SPARQL},
        RDFStoreError,
    },
    std::{borrow::Cow, ffi::CString, ops::Deref, sync::Arc},
};

/// SPARQL Statement
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Statement {
    pub prefixes:    Arc<Namespaces>,
    pub(crate) text: String,
}

impl Display for Statement {
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
        writeln!(f, "SPARQL Statement:")?;
        for (number, line) in self.text.lines().enumerate() {
            writeln!(f, "{:0>4}: {line}", number + 1)?;
        }
        Ok(())
    }
}

impl Statement {
    pub fn new(prefixes: &Arc<Namespaces>, statement: Cow<str>) -> Result<Self, RDFStoreError> {
        let s = Self {
            prefixes: prefixes.clone(),
            text:     format!("{}\n{}", &prefixes.to_string(), statement.trim()),
        };
        tracing::trace!(target: LOG_TARGET_SPARQL, "{:}", s);
        Ok(s)
    }

    pub fn cursor(
        &self,
        connection: &Arc<DataStoreConnection>,
        parameters: &Parameters,
    ) -> Result<Cursor, RDFStoreError> {
        Cursor::create(connection, parameters, self)
    }

    pub(crate) fn as_c_string(&self) -> Result<CString, RDFStoreError> {
        Ok(CString::new(self.text.as_str())?)
    }

    pub fn as_str(&self) -> &str { self.text.as_str() }

    pub fn no_comments(&self) -> String { no_comments(self.text.as_str()) }

    /// Return a Statement that can be used to export all data in
    /// `application/nquads` format
    pub fn nquads_query(prefixes: &Arc<Namespaces>) -> Result<Statement, RDFStoreError> {
        let default_graph = DEFAULT_GRAPH_RDFOX.deref().as_display_iri();
        let statement = Statement::new(
            prefixes,
            formatdoc!(
                r##"
                SELECT ?S ?P ?O ?G
                WHERE {{
                    {{
                        GRAPH ?G {{ ?S ?P ?O }}
                    }} UNION {{
                        ?S ?P ?P .
                        BIND({default_graph} AS ?G)
                    }}
                }}
            "##
            )
            .into(),
        )?;
        Ok(statement)
    }
}

pub fn no_comments(string: &str) -> String {
    use std::fmt::Write;

    let re = fancy_regex::Regex::new(r"(.*)(?!#>)#.*$").unwrap();

    let do_line = |line: &str| -> (bool, String) {
        let caps = re.captures(line);
        if let Ok(Some(caps)) = caps {
            let mat = caps.get(1).unwrap();
            (
                true,
                line[mat.start()..mat.end()].trim_end().to_string(),
            )
        } else {
            (false, line.trim_end().to_string())
        }
    };

    let mut output = String::new();
    for line in string.lines() {
        let mut line = line.to_string();
        loop {
            let (again, result) = do_line(line.as_str());
            if again {
                // Repeat the call to do_line again to make sure that all #-comments are removed
                // (there could be multiple on one line)
                line = result;
            } else {
                writeln!(&mut output, "{result}").unwrap();
                break
            }
        }
    }
    output
}

#[cfg(test)]
mod tests {
    use {crate::statement::no_comments, indoc::formatdoc};

    #[test_log::test]
    fn test_no_comments() {
        let sparql = formatdoc! {r##"
            PREFIX abc: <https://whatever.org#> # focus on this and the next line
            PREFIX owl: <http://www.w3.org/2002/07/owl#>
            SELECT DISTINCT ?thing
            WHERE {{
                {{ # some comment
                    GRAPH ?graph {{ # more # and more
                        ?thing a Whatever#
                    }}
                }} UNION {{
                    ?thing a Whatever .# abc
                                       # def
                    BIND(graph:Graph AS ?graph)
                }}
            }}
            "##
        };
        let expected = formatdoc! {r##"
            PREFIX abc: <https://whatever.org#>
            PREFIX owl: <http://www.w3.org/2002/07/owl#>
            SELECT DISTINCT ?thing
            WHERE {{
                {{
                    GRAPH ?graph {{
                        ?thing a Whatever
                    }}
                }} UNION {{
                    ?thing a Whatever .

                    BIND(graph:Graph AS ?graph)
                }}
            }}
            "##
        };
        let actual = no_comments(sparql.as_str());
        assert_eq!(actual.as_str(), expected.as_str());
    }
}