1use serde::{Deserialize, Serialize};
21use utoipa::ToSchema;
22use uuid::Uuid;
23
24#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
33#[serde(rename_all = "camelCase")]
34pub struct SelectTenantDTO {
35 #[serde(default, alias = "tenant_id")]
36 pub tenant_id: Option<Uuid>,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
43#[serde(transparent)]
44pub struct GrantDatasetPermissionBody(pub Vec<Uuid>);
45
46#[derive(Debug, Clone, Deserialize, ToSchema)]
49#[serde(rename_all = "snake_case")]
50pub struct GrantDatasetPermissionQuery {
51 pub permission_name: String,
52}
53
54#[derive(Debug, Clone, Deserialize, ToSchema)]
55#[serde(rename_all = "snake_case")]
56pub struct CreateRoleQuery {
57 pub role_name: String,
58}
59
60#[derive(Debug, Clone, Deserialize, ToSchema)]
61#[serde(rename_all = "snake_case")]
62pub struct CreateTenantQuery {
63 pub tenant_name: String,
64}
65
66#[derive(Debug, Clone, Deserialize, ToSchema)]
67#[serde(rename_all = "snake_case")]
68pub struct AssignRoleQuery {
69 pub role_id: Uuid,
70}
71
72#[derive(Debug, Clone, Deserialize, ToSchema)]
73#[serde(rename_all = "snake_case")]
74pub struct AddUserToTenantQuery {
75 pub tenant_id: Uuid,
76}
77
78#[derive(Debug, Clone, Deserialize, ToSchema)]
80#[serde(rename_all = "snake_case")]
81pub struct RevokeDatasetPermissionQuery {
82 pub permission_name: String,
84}
85
86#[derive(Debug, Clone, Deserialize, ToSchema)]
88#[serde(rename_all = "snake_case")]
89pub struct RemoveUserFromRoleQuery {
90 pub role_id: Uuid,
91}
92
93#[derive(Debug, Clone, Serialize, ToSchema)]
96#[serde(rename_all = "snake_case")]
97pub struct MessageResponse {
98 pub message: String,
99}
100
101#[derive(Debug, Clone, Serialize, ToSchema)]
102#[serde(rename_all = "snake_case")]
103pub struct CreateRoleResponse {
104 pub message: String,
105 pub role_id: Uuid,
106 pub tenant_id: Uuid,
107}
108
109#[derive(Debug, Clone, Serialize, ToSchema)]
110#[serde(rename_all = "snake_case")]
111pub struct CreateTenantResponse {
112 pub message: String,
113 pub tenant_id: Uuid,
114}
115
116#[derive(Debug, Clone, Serialize, ToSchema)]
122pub struct SelectTenantResponse {
123 pub message: String,
124 #[serde(serialize_with = "serialize_tenant_id_with_none_literal")]
127 pub tenant_id: Option<Uuid>,
128}
129
130fn serialize_tenant_id_with_none_literal<S>(
131 value: &Option<Uuid>,
132 serializer: S,
133) -> Result<S::Ok, S::Error>
134where
135 S: serde::Serializer,
136{
137 match value {
138 Some(uuid) => serializer.serialize_str(&uuid.hyphenated().to_string()),
139 None => serializer.serialize_str("None"),
140 }
141}
142
143#[derive(Debug, Clone, Serialize, ToSchema)]
144#[serde(rename_all = "snake_case")]
145pub struct TenantSummary {
146 pub id: Uuid,
147 pub name: String,
148}
149
150#[derive(Debug, Clone, Serialize, ToSchema)]
151#[serde(rename_all = "snake_case")]
152pub struct RoleSummary {
153 pub id: Uuid,
154 pub name: String,
155 pub description: Option<String>,
158 pub user_count: Option<usize>,
161}
162
163#[derive(Debug, Clone, Serialize, ToSchema)]
164#[serde(rename_all = "snake_case")]
165pub struct UserInRole {
166 pub id: Uuid,
167 pub name: String,
169}
170
171#[derive(Debug, Clone, Serialize, ToSchema)]
172#[serde(rename_all = "snake_case")]
173pub struct UserInTenant {
174 pub id: Uuid,
175 pub email: String,
176 pub roles: Vec<RoleSummary>,
178}
179
180#[cfg(test)]
181#[allow(
182 clippy::unwrap_used,
183 clippy::expect_used,
184 reason = "test code — panics are acceptable failures"
185)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn select_tenant_dto_accepts_null_tenant_id() {
191 let parsed: SelectTenantDTO =
193 serde_json::from_str(r#"{"tenant_id": null}"#).expect("parse");
194 assert_eq!(parsed.tenant_id, None);
195 }
196
197 #[test]
198 fn select_tenant_dto_accepts_missing_field() {
199 let parsed: SelectTenantDTO = serde_json::from_str("{}").expect("parse");
200 assert_eq!(parsed.tenant_id, None);
201 }
202
203 #[test]
204 fn select_tenant_dto_accepts_camelcase_input() {
205 let parsed: SelectTenantDTO =
206 serde_json::from_str(r#"{"tenantId": "00000000-0000-0000-0000-000000000001"}"#)
207 .expect("parse camelCase");
208 assert_eq!(
209 parsed.tenant_id,
210 Some(Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("uuid"))
211 );
212 }
213
214 #[test]
215 fn select_tenant_dto_accepts_snake_case_input_via_alias() {
216 let parsed: SelectTenantDTO =
217 serde_json::from_str(r#"{"tenant_id": "00000000-0000-0000-0000-000000000001"}"#)
218 .expect("parse snake_case");
219 assert_eq!(
220 parsed.tenant_id,
221 Some(Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("uuid"))
222 );
223 }
224
225 #[test]
226 fn select_tenant_dto_serializes_camelcase_only() {
227 let dto = SelectTenantDTO {
228 tenant_id: Some(Uuid::nil()),
229 };
230 let s = serde_json::to_string(&dto).expect("serialize");
231 assert!(s.contains("\"tenantId\""), "missing tenantId: {s}");
232 assert!(
233 !s.contains("\"tenant_id\""),
234 "snake_case tenant_id leaked: {s}"
235 );
236 }
237
238 #[test]
239 fn select_tenant_response_serializes_none_as_string() {
240 let r = SelectTenantResponse {
241 message: "Tenant selected.".into(),
242 tenant_id: None,
243 };
244 let s = serde_json::to_string(&r).expect("serialize");
245 assert!(s.contains(r#""tenant_id":"None""#));
246 }
247
248 #[test]
249 fn select_tenant_response_serializes_uuid_as_string() {
250 let id = Uuid::nil();
251 let r = SelectTenantResponse {
252 message: "Tenant selected.".into(),
253 tenant_id: Some(id),
254 };
255 let s = serde_json::to_string(&r).expect("serialize");
256 assert!(s.contains("00000000-0000-0000-0000-000000000000"));
257 }
258}