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}
64
65impl Request {
66 #[must_use]
67 pub fn operation_label(&self) -> &'static str {
68 match self {
69 Request::Create { .. } => "create",
70 Request::Read { .. } => "read",
71 Request::Update { .. } => "update",
72 Request::Delete { .. } => "delete",
73 Request::List { .. } => "list",
74 Request::Subscribe { .. } => "subscribe",
75 Request::Unsubscribe { .. } => "unsubscribe",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum ErrorCode {
82 BadRequest = 400,
83 Forbidden = 403,
84 NotFound = 404,
85 Conflict = 409,
86 Internal = 500,
87}
88
89impl ErrorCode {
90 #[must_use]
91 pub fn as_u16(self) -> u16 {
92 self as u16
93 }
94
95 #[must_use]
96 pub fn as_grpc_code(self) -> i32 {
97 match self {
98 ErrorCode::BadRequest => 3,
99 ErrorCode::Forbidden => 7,
100 ErrorCode::NotFound => 5,
101 ErrorCode::Conflict => 6,
102 ErrorCode::Internal => 13,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ErrorResponse {
109 pub code: ErrorCode,
110 pub message: String,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "status", rename_all = "snake_case")]
115pub enum Response {
116 Ok { data: Value },
117 Error { code: u16, message: String },
118}
119
120impl Response {
121 #[must_use]
122 pub fn ok(data: Value) -> Self {
123 Response::Ok { data }
124 }
125
126 pub fn error(code: ErrorCode, message: impl Into<String>) -> Self {
127 Response::Error {
128 code: code.as_u16(),
129 message: message.into(),
130 }
131 }
132
133 #[must_use]
134 pub fn is_ok(&self) -> bool {
135 matches!(self, Response::Ok { .. })
136 }
137
138 #[must_use]
139 pub fn is_error(&self) -> bool {
140 matches!(self, Response::Error { .. })
141 }
142}
143
144impl From<Error> for Response {
145 fn from(e: Error) -> Self {
146 let (code, message) = match &e {
147 Error::NotFound { entity, .. } => (ErrorCode::NotFound, format!("not found: {entity}")),
148 Error::Validation(msg) => (ErrorCode::BadRequest, format!("validation error: {msg}")),
149 Error::SchemaViolation { field, reason, .. } => (
150 ErrorCode::BadRequest,
151 format!("schema validation failed: {field} - {reason}"),
152 ),
153 Error::Forbidden(_) => (ErrorCode::Forbidden, "forbidden".to_string()),
154 Error::ConstraintViolation(msg) => {
155 (ErrorCode::Conflict, format!("constraint violation: {msg}"))
156 }
157 Error::UniqueViolation { entity, field, .. } => (
158 ErrorCode::Conflict,
159 format!("unique constraint violation: {entity}.{field}"),
160 ),
161 Error::ForeignKeyViolation { entity, field, .. } => (
162 ErrorCode::Conflict,
163 format!("foreign key violation: {entity}.{field}"),
164 ),
165 Error::ForeignKeyRestrict { entity, .. } => (
166 ErrorCode::Conflict,
167 format!("cannot delete {entity}: referenced by other entities"),
168 ),
169 Error::NotNullViolation { entity, field } => (
170 ErrorCode::Conflict,
171 format!("not null violation: {entity}.{field}"),
172 ),
173 Error::CascadeBlocked(info) => (
174 ErrorCode::Conflict,
175 format!(
176 "cascade blocked: {}/{} owned by '{}' has non-nullable FK field '{}'",
177 info.blocked_entity, info.blocked_id, info.blocked_owner, info.blocked_field
178 ),
179 ),
180 Error::Conflict(msg) => (ErrorCode::Conflict, format!("conflict: {msg}")),
181 _ => {
182 tracing::error!(error = %e, "internal error in client request");
183 (ErrorCode::Internal, "internal error".to_string())
184 }
185 };
186 Response::error(code, message)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_request_serialization() {
196 let request = Request::Create {
197 entity: "users".to_string(),
198 data: serde_json::json!({"name": "Alice"}),
199 };
200 let json = serde_json::to_string(&request).unwrap();
201 assert!(json.contains("\"op\":\"create\""));
202 assert!(json.contains("\"entity\":\"users\""));
203 }
204
205 #[test]
206 fn test_request_deserialization() {
207 let json = r#"{"op": "read", "entity": "users", "id": "123"}"#;
208 let request: Request = serde_json::from_str(json).unwrap();
209 match request {
210 Request::Read { entity, id, .. } => {
211 assert_eq!(entity, "users");
212 assert_eq!(id, "123");
213 }
214 _ => panic!("expected Read request"),
215 }
216 }
217
218 #[test]
219 fn test_response_ok() {
220 let response = Response::ok(serde_json::json!({"id": "1", "name": "Alice"}));
221 assert!(response.is_ok());
222 let json = serde_json::to_string(&response).unwrap();
223 assert!(json.contains("\"status\":\"ok\""));
224 }
225
226 #[test]
227 fn test_response_error() {
228 let response = Response::error(ErrorCode::NotFound, "User not found");
229 assert!(response.is_error());
230 let json = serde_json::to_string(&response).unwrap();
231 assert!(json.contains("\"status\":\"error\""));
232 assert!(json.contains("\"code\":404"));
233 }
234
235 #[test]
236 fn test_error_code_mapping() {
237 assert_eq!(ErrorCode::NotFound.as_u16(), 404);
238 assert_eq!(ErrorCode::BadRequest.as_u16(), 400);
239 assert_eq!(ErrorCode::Conflict.as_u16(), 409);
240 assert_eq!(ErrorCode::Internal.as_u16(), 500);
241
242 assert_eq!(ErrorCode::NotFound.as_grpc_code(), 5);
243 assert_eq!(ErrorCode::BadRequest.as_grpc_code(), 3);
244 }
245
246 #[test]
247 fn test_error_conversion() {
248 let error = Error::NotFound {
249 entity: "users".to_string(),
250 id: "123".to_string(),
251 };
252 let response: Response = error.into();
253 match response {
254 Response::Error { code, message } => {
255 assert_eq!(code, 404);
256 assert!(message.contains("not found"));
257 }
258 Response::Ok { .. } => panic!("expected error response"),
259 }
260 }
261}