Skip to main content

kglite_c/
status.rs

1//! Status-code surface: `KgliteStatusCode` enum + 1:1 mapping to
2//! `kglite::api::KgErrorCode` + the three canonical accessors
3//! (`name`, `neo4j_status`, `http_status`).
4//!
5//! The mapping is fixed in declaration order and the discriminants
6//! are stable for the lifetime of the ABI major version. Adding a
7//! new `KgErrorCode` variant in core appends a new status code
8//! here; removing one would require an ABI-major bump.
9
10use crate::strings::alloc_c_string;
11use kglite::api::KgErrorCode;
12use std::ffi::c_char;
13
14/// C-ABI-side error code. Variants 1-16 map 1:1 to
15/// [`kglite::api::KgErrorCode`]; variants 100+ are C-ABI-specific
16/// (invalid UTF-8 at the boundary, null pointer, OOM — conditions
17/// that don't have a corresponding `KgErrorCode` because they
18/// can't arise from inside the engine).
19#[repr(u32)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum KgliteStatusCode {
22    Ok = 0,
23
24    // 1-16: mirror KgErrorCode, same order as declared there.
25    CypherSyntax = 1,
26    CypherTimeout = 2,
27    CypherExecution = 3,
28    CypherTypeMismatch = 4,
29    Schema = 5,
30    Validation = 6,
31    Expr = 7,
32    NodeNotFound = 8,
33    ConnectionNotFound = 9,
34    PropertyNotFound = 10,
35    FileNotFound = 11,
36    FileFormat = 12,
37    FileIo = 13,
38    InvalidArgument = 14,
39    MissingArgument = 15,
40    Internal = 16,
41
42    // 100+: C-ABI-only errors.
43    /// A string argument failed UTF-8 validation. The C-side
44    /// caller passed a `*const c_char` whose bytes didn't decode
45    /// as UTF-8 — typically a corrupted buffer or a non-UTF-8
46    /// locale string. kglite is UTF-8 throughout.
47    InvalidUtf8 = 100,
48    /// A required pointer argument was null. The function
49    /// can't proceed; check your call site.
50    NullPointer = 101,
51}
52
53impl KgliteStatusCode {
54    /// Map a core `KgErrorCode` variant to its C-ABI counterpart.
55    /// Inline-callable from anywhere in the crate (used by every
56    /// fallible wrapper that catches a `KgError` from the engine).
57    pub(crate) fn from_kg_error_code(code: KgErrorCode) -> Self {
58        match code {
59            KgErrorCode::CypherSyntax => Self::CypherSyntax,
60            KgErrorCode::CypherTimeout => Self::CypherTimeout,
61            KgErrorCode::CypherExecution => Self::CypherExecution,
62            KgErrorCode::CypherTypeMismatch => Self::CypherTypeMismatch,
63            KgErrorCode::Schema => Self::Schema,
64            KgErrorCode::Validation => Self::Validation,
65            KgErrorCode::Expr => Self::Expr,
66            KgErrorCode::NodeNotFound => Self::NodeNotFound,
67            KgErrorCode::ConnectionNotFound => Self::ConnectionNotFound,
68            KgErrorCode::PropertyNotFound => Self::PropertyNotFound,
69            KgErrorCode::FileNotFound => Self::FileNotFound,
70            KgErrorCode::FileFormat => Self::FileFormat,
71            KgErrorCode::FileIo => Self::FileIo,
72            KgErrorCode::InvalidArgument => Self::InvalidArgument,
73            KgErrorCode::MissingArgument => Self::MissingArgument,
74            KgErrorCode::Internal => Self::Internal,
75        }
76    }
77
78    /// Reverse: C-ABI code → `KgErrorCode` so the helper accessors
79    /// can delegate. Returns `None` for `Ok` and the C-ABI-only
80    /// codes (`InvalidUtf8`, `NullPointer`) which have no
81    /// `KgErrorCode` counterpart.
82    pub(crate) fn to_kg_error_code(self) -> Option<KgErrorCode> {
83        Some(match self {
84            Self::Ok | Self::InvalidUtf8 | Self::NullPointer => return None,
85            Self::CypherSyntax => KgErrorCode::CypherSyntax,
86            Self::CypherTimeout => KgErrorCode::CypherTimeout,
87            Self::CypherExecution => KgErrorCode::CypherExecution,
88            Self::CypherTypeMismatch => KgErrorCode::CypherTypeMismatch,
89            Self::Schema => KgErrorCode::Schema,
90            Self::Validation => KgErrorCode::Validation,
91            Self::Expr => KgErrorCode::Expr,
92            Self::NodeNotFound => KgErrorCode::NodeNotFound,
93            Self::ConnectionNotFound => KgErrorCode::ConnectionNotFound,
94            Self::PropertyNotFound => KgErrorCode::PropertyNotFound,
95            Self::FileNotFound => KgErrorCode::FileNotFound,
96            Self::FileFormat => KgErrorCode::FileFormat,
97            Self::FileIo => KgErrorCode::FileIo,
98            Self::InvalidArgument => KgErrorCode::InvalidArgument,
99            Self::MissingArgument => KgErrorCode::MissingArgument,
100            Self::Internal => KgErrorCode::Internal,
101        })
102    }
103}
104
105/// Return the canonical human-readable name of a status code (e.g.
106/// `"CypherSyntax"`, `"NodeNotFound"`, `"InvalidUtf8"`).
107///
108/// The returned string is OWNED by the caller and must be freed
109/// via [`kglite_free_string`](crate::kglite_free_string). Returns
110/// null on `Ok` (no error to name).
111#[no_mangle]
112pub extern "C" fn kglite_status_code_name(code: KgliteStatusCode) -> *const c_char {
113    let s = match code {
114        KgliteStatusCode::Ok => return std::ptr::null(),
115        KgliteStatusCode::InvalidUtf8 => "InvalidUtf8",
116        KgliteStatusCode::NullPointer => "NullPointer",
117        other => match other.to_kg_error_code() {
118            Some(kg) => kg.as_str(),
119            None => return std::ptr::null(),
120        },
121    };
122    alloc_c_string(s)
123}
124
125/// Return the Neo4j wire status code for a status code (e.g.
126/// `"Neo.ClientError.Statement.SyntaxError"`). Useful for bindings
127/// implementing the Neo4j Bolt wire protocol or compatible HTTP
128/// APIs.
129///
130/// The returned string is OWNED by the caller and must be freed
131/// via [`kglite_free_string`](crate::kglite_free_string). Returns
132/// null on `Ok` or on C-ABI-only error codes that have no Neo4j
133/// counterpart (`InvalidUtf8`, `NullPointer`).
134#[no_mangle]
135pub extern "C" fn kglite_status_code_neo4j_status(code: KgliteStatusCode) -> *const c_char {
136    match code.to_kg_error_code() {
137        Some(kg) => alloc_c_string(kg.neo4j_status_code()),
138        None => std::ptr::null(),
139    }
140}
141
142/// Return the HTTP status code mapping for a status code (e.g.
143/// 400 for `CypherSyntax`, 404 for `NodeNotFound`, 500 for
144/// `Internal`). Useful for REST/gRPC bindings.
145///
146/// Returns 0 for `Ok` and 500 for C-ABI-only codes (`InvalidUtf8`
147/// = 400 / bad request from caller, `NullPointer` = 400).
148#[no_mangle]
149pub extern "C" fn kglite_status_code_http_status(code: KgliteStatusCode) -> u16 {
150    match code {
151        KgliteStatusCode::Ok => 0,
152        KgliteStatusCode::InvalidUtf8 | KgliteStatusCode::NullPointer => 400,
153        other => match other.to_kg_error_code() {
154            Some(kg) => kg.http_status_code(),
155            None => 500,
156        },
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn every_kg_error_code_round_trips() {
166        // Exhaustive check — every KgErrorCode maps to a
167        // KgliteStatusCode and back.
168        for code in [
169            KgErrorCode::CypherSyntax,
170            KgErrorCode::CypherTimeout,
171            KgErrorCode::CypherExecution,
172            KgErrorCode::CypherTypeMismatch,
173            KgErrorCode::Schema,
174            KgErrorCode::Validation,
175            KgErrorCode::Expr,
176            KgErrorCode::NodeNotFound,
177            KgErrorCode::ConnectionNotFound,
178            KgErrorCode::PropertyNotFound,
179            KgErrorCode::FileNotFound,
180            KgErrorCode::FileFormat,
181            KgErrorCode::FileIo,
182            KgErrorCode::InvalidArgument,
183            KgErrorCode::MissingArgument,
184            KgErrorCode::Internal,
185        ] {
186            let c = KgliteStatusCode::from_kg_error_code(code);
187            let back = c.to_kg_error_code();
188            assert_eq!(back, Some(code), "round-trip failed for {code:?}");
189        }
190    }
191
192    #[test]
193    fn http_status_helpers_match_core() {
194        // Sanity: an arbitrary code matches what core says.
195        assert_eq!(
196            kglite_status_code_http_status(KgliteStatusCode::CypherSyntax),
197            400
198        );
199        assert_eq!(
200            kglite_status_code_http_status(KgliteStatusCode::NodeNotFound),
201            404
202        );
203        assert_eq!(
204            kglite_status_code_http_status(KgliteStatusCode::Internal),
205            500
206        );
207        assert_eq!(kglite_status_code_http_status(KgliteStatusCode::Ok), 0);
208    }
209}