1use crate::subscription::SubscriptionMode;
5use crate::{Error, Filter, Pagination, SortOrder};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub enum VaultConstraintData {
10 Create(Value),
11 Update(Value, Value),
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "op", rename_all = "snake_case")]
16pub enum Request {
17 Create {
18 entity: String,
19 data: Value,
20 },
21 Read {
22 entity: String,
23 id: String,
24 #[serde(default)]
25 includes: Vec<String>,
26 #[serde(default)]
27 projection: Option<Vec<String>>,
28 },
29 Update {
30 entity: String,
31 id: String,
32 fields: Value,
33 },
34 Delete {
35 entity: String,
36 id: String,
37 },
38 List {
39 entity: String,
40 #[serde(default)]
41 filters: Vec<Filter>,
42 #[serde(default)]
43 sort: Vec<SortOrder>,
44 #[serde(default)]
45 pagination: Option<Pagination>,
46 #[serde(default)]
47 includes: Vec<String>,
48 #[serde(default)]
49 projection: Option<Vec<String>>,
50 },
51 Subscribe {
52 pattern: String,
53 #[serde(default)]
54 entity: Option<String>,
55 #[serde(default)]
56 share_group: Option<String>,
57 #[serde(default)]
58 mode: Option<SubscriptionMode>,
59 },
60 Unsubscribe {
61 id: String,
62 },
63 Share {
64 entity: String,
65 id: String,
66 grantee: String,
67 permission: String,
68 #[serde(default = "default_cascade")]
69 cascade: bool,
70 },
71 Unshare {
72 entity: String,
73 id: String,
74 grantee: String,
75 #[serde(default = "default_cascade")]
76 cascade: bool,
77 },
78 Shares {
79 entity: String,
80 id: String,
81 },
82 Shared {
83 entity: String,
84 },
85}
86
87fn default_cascade() -> bool {
88 true
89}
90
91impl Request {
92 #[must_use]
93 pub fn operation_label(&self) -> &'static str {
94 match self {
95 Request::Create { .. } => "create",
96 Request::Read { .. } => "read",
97 Request::Update { .. } => "update",
98 Request::Delete { .. } => "delete",
99 Request::List { .. } => "list",
100 Request::Subscribe { .. } => "subscribe",
101 Request::Unsubscribe { .. } => "unsubscribe",
102 Request::Share { .. } => "share",
103 Request::Unshare { .. } => "unshare",
104 Request::Shares { .. } => "shares",
105 Request::Shared { .. } => "shared",
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum ErrorCode {
112 BadRequest = 400,
113 Unauthorized = 401,
114 Forbidden = 403,
115 NotFound = 404,
116 Conflict = 409,
117 RateLimited = 429,
118 Internal = 500,
119}
120
121impl ErrorCode {
122 #[must_use]
123 pub fn as_u16(self) -> u16 {
124 self as u16
125 }
126
127 #[must_use]
128 pub fn as_grpc_code(self) -> i32 {
129 match self {
130 ErrorCode::BadRequest => 3,
131 ErrorCode::Unauthorized => 16,
132 ErrorCode::Forbidden => 7,
133 ErrorCode::NotFound => 5,
134 ErrorCode::Conflict => 6,
135 ErrorCode::RateLimited => 8,
136 ErrorCode::Internal => 13,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ErrorResponse {
143 pub code: ErrorCode,
144 pub message: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(tag = "status", rename_all = "snake_case")]
149pub enum Response {
150 Ok { data: Value },
151 Error { code: u16, message: String },
152}
153
154impl Response {
155 #[must_use]
156 pub fn ok(data: Value) -> Self {
157 Response::Ok { data }
158 }
159
160 pub fn error(code: ErrorCode, message: impl Into<String>) -> Self {
161 Response::Error {
162 code: code.as_u16(),
163 message: message.into(),
164 }
165 }
166
167 #[must_use]
168 pub fn is_ok(&self) -> bool {
169 matches!(self, Response::Ok { .. })
170 }
171
172 #[must_use]
173 pub fn is_error(&self) -> bool {
174 matches!(self, Response::Error { .. })
175 }
176}
177
178impl From<Error> for Response {
179 fn from(e: Error) -> Self {
180 let (code, message) = match &e {
181 Error::NotFound { entity, .. } => (ErrorCode::NotFound, format!("not found: {entity}")),
182 Error::Validation(msg) => (ErrorCode::BadRequest, format!("validation error: {msg}")),
183 Error::SchemaViolation { field, reason, .. } => (
184 ErrorCode::BadRequest,
185 format!("schema validation failed: {field} - {reason}"),
186 ),
187 Error::Forbidden(_) => (ErrorCode::Forbidden, "forbidden".to_string()),
188 Error::ConstraintViolation(msg) => {
189 (ErrorCode::Conflict, format!("constraint violation: {msg}"))
190 }
191 Error::UniqueViolation { entity, field, .. } => (
192 ErrorCode::Conflict,
193 format!("unique constraint violation: {entity}.{field}"),
194 ),
195 Error::ForeignKeyViolation { entity, field, .. } => (
196 ErrorCode::Conflict,
197 format!("foreign key violation: {entity}.{field}"),
198 ),
199 Error::ForeignKeyRestrict { entity, .. } => (
200 ErrorCode::Conflict,
201 format!("cannot delete {entity}: referenced by other entities"),
202 ),
203 Error::NotNullViolation { entity, field } => (
204 ErrorCode::Conflict,
205 format!("not null violation: {entity}.{field}"),
206 ),
207 Error::CascadeBlocked(info) => (
208 ErrorCode::Conflict,
209 format!(
210 "cascade blocked: {}/{} owned by '{}' has non-nullable FK field '{}'",
211 info.blocked_entity, info.blocked_id, info.blocked_owner, info.blocked_field
212 ),
213 ),
214 Error::Conflict(msg) => (ErrorCode::Conflict, format!("conflict: {msg}")),
215 _ => {
216 tracing::error!(error = %e, "internal error in client request");
217 (ErrorCode::Internal, "internal error".to_string())
218 }
219 };
220 Response::error(code, message)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_request_serialization() {
230 let request = Request::Create {
231 entity: "users".to_string(),
232 data: serde_json::json!({"name": "Alice"}),
233 };
234 let json = serde_json::to_string(&request).unwrap();
235 assert!(json.contains("\"op\":\"create\""));
236 assert!(json.contains("\"entity\":\"users\""));
237 }
238
239 #[test]
240 fn test_request_deserialization() {
241 let json = r#"{"op": "read", "entity": "users", "id": "123"}"#;
242 let request: Request = serde_json::from_str(json).unwrap();
243 match request {
244 Request::Read { entity, id, .. } => {
245 assert_eq!(entity, "users");
246 assert_eq!(id, "123");
247 }
248 _ => panic!("expected Read request"),
249 }
250 }
251
252 #[test]
253 fn test_response_ok() {
254 let response = Response::ok(serde_json::json!({"id": "1", "name": "Alice"}));
255 assert!(response.is_ok());
256 let json = serde_json::to_string(&response).unwrap();
257 assert!(json.contains("\"status\":\"ok\""));
258 }
259
260 #[test]
261 fn test_response_error() {
262 let response = Response::error(ErrorCode::NotFound, "User not found");
263 assert!(response.is_error());
264 let json = serde_json::to_string(&response).unwrap();
265 assert!(json.contains("\"status\":\"error\""));
266 assert!(json.contains("\"code\":404"));
267 }
268
269 #[test]
270 fn test_error_code_mapping() {
271 assert_eq!(ErrorCode::NotFound.as_u16(), 404);
272 assert_eq!(ErrorCode::BadRequest.as_u16(), 400);
273 assert_eq!(ErrorCode::Conflict.as_u16(), 409);
274 assert_eq!(ErrorCode::Internal.as_u16(), 500);
275
276 assert_eq!(ErrorCode::NotFound.as_grpc_code(), 5);
277 assert_eq!(ErrorCode::BadRequest.as_grpc_code(), 3);
278 }
279
280 #[test]
281 fn test_error_conversion() {
282 let error = Error::NotFound {
283 entity: "users".to_string(),
284 id: "123".to_string(),
285 };
286 let response: Response = error.into();
287 match response {
288 Response::Error { code, message } => {
289 assert_eq!(code, 404);
290 assert!(message.contains("not found"));
291 }
292 Response::Ok { .. } => panic!("expected error response"),
293 }
294 }
295}