1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Deserializer, Serialize};
8
9fn string_or_none<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
14 let value: Option<serde_json::Value> = Option::deserialize(d)?;
15 Ok(value.and_then(|v| match v {
16 serde_json::Value::String(s) => Some(s),
17 _ => None,
18 }))
19}
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct PaginatedResponse<T> {
28 pub results: Vec<T>,
30 pub pagination: PaginationInfo,
32}
33
34#[derive(Debug, Clone, Deserialize, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct PaginationInfo {
38 pub limit: usize,
40 pub offset: usize,
42 pub total_results: usize,
44}
45
46impl<T> PaginatedResponse<T> {
47 pub fn has_more(&self) -> bool {
49 self.pagination.offset + self.results.len() < self.pagination.total_results
50 }
51
52 pub fn next_offset(&self) -> usize {
54 self.pagination.offset + self.pagination.limit
55 }
56
57 pub fn is_first_page(&self) -> bool {
59 self.pagination.offset == 0
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
69#[serde(rename_all = "lowercase")]
70pub enum ProjectClassification {
71 Production,
73 Template,
75 Component,
77 Sample,
79}
80
81impl std::fmt::Display for ProjectClassification {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Self::Production => write!(f, "production"),
85 Self::Template => write!(f, "template"),
86 Self::Component => write!(f, "component"),
87 Self::Sample => write!(f, "sample"),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct AccountInfo {
100 pub id: String,
102 pub name: String,
104 #[serde(default)]
106 pub region: Option<String>,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct AccountUser {
113 pub id: String,
115 pub email: String,
117 #[serde(default)]
119 pub name: Option<String>,
120 #[serde(default)]
122 pub first_name: Option<String>,
123 #[serde(default)]
125 pub last_name: Option<String>,
126 #[serde(default)]
128 pub company_id: Option<String>,
129 #[serde(default)]
131 pub status: Option<String>,
132 #[serde(default)]
134 pub added_on: Option<DateTime<Utc>>,
135}
136
137impl AccountUser {
138 pub fn display_name(&self) -> &str {
140 self.name.as_deref().unwrap_or(&self.email)
141 }
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct AccountProject {
152 pub id: String,
154 pub name: String,
156 #[serde(default, deserialize_with = "string_or_none")]
158 pub status: Option<String>,
159 #[serde(default, deserialize_with = "string_or_none")]
161 pub platform: Option<String>,
162 #[serde(default, deserialize_with = "string_or_none")]
164 pub account_id: Option<String>,
165 #[serde(default)]
167 pub created_at: Option<DateTime<Utc>>,
168 #[serde(default)]
170 pub updated_at: Option<DateTime<Utc>>,
171 #[serde(default, deserialize_with = "string_or_none", alias = "projectType")]
173 pub project_type: Option<String>,
174 #[serde(default)]
176 pub classification: Option<ProjectClassification>,
177 #[serde(default)]
179 pub member_count: Option<usize>,
180 #[serde(default)]
182 pub company_count: Option<usize>,
183 #[serde(default)]
185 pub products: Option<Vec<serde_json::Value>>,
186}
187
188impl AccountProject {
189 pub fn is_acc(&self) -> bool {
191 self.platform
192 .as_ref()
193 .map(|p| p.to_lowercase() == "acc")
194 .unwrap_or(false)
195 || self
196 .project_type
197 .as_ref()
198 .map(|t| t.to_lowercase().contains("acc"))
199 .unwrap_or(false)
200 }
201
202 pub fn is_bim360(&self) -> bool {
204 self.platform
205 .as_ref()
206 .map(|p| p.to_lowercase().contains("bim360") || p.to_lowercase().contains("bim 360"))
207 .unwrap_or(false)
208 || self
209 .project_type
210 .as_ref()
211 .map(|t| t.to_lowercase().contains("bim"))
212 .unwrap_or(false)
213 }
214
215 pub fn is_active(&self) -> bool {
217 self.status
218 .as_ref()
219 .map(|s| s.to_lowercase() == "active")
220 .unwrap_or(true)
221 }
222
223 pub fn is_template(&self) -> bool {
225 self.classification == Some(ProjectClassification::Template)
226 }
227
228 pub fn enabled_products(&self) -> Vec<String> {
230 self.products
231 .as_ref()
232 .map(|products| {
233 products
234 .iter()
235 .filter_map(|v| {
236 v.as_object()
238 .and_then(|obj| obj.get("key"))
239 .and_then(|k| k.as_str())
240 .map(String::from)
241 .or_else(|| v.as_str().map(String::from))
243 })
244 .collect()
245 })
246 .unwrap_or_default()
247 }
248}
249
250#[derive(Debug, Clone, Deserialize, Serialize)]
256#[serde(rename_all = "camelCase")]
257pub struct ProjectUser {
258 pub id: String,
260 #[serde(default)]
262 pub email: Option<String>,
263 #[serde(default)]
265 pub name: Option<String>,
266 #[serde(default)]
268 pub role_id: Option<String>,
269 #[serde(default)]
271 pub role_name: Option<String>,
272 #[serde(default)]
274 pub products: Option<Vec<ProductAccess>>,
275 #[serde(default)]
277 pub added_on: Option<DateTime<Utc>>,
278}
279
280#[derive(Debug, Clone, Deserialize, Serialize)]
282#[serde(rename_all = "camelCase")]
283pub struct ProductAccess {
284 pub key: String,
286 pub access: String,
288}
289
290#[derive(Debug, Clone, Deserialize, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct Company {
298 pub id: String,
300 pub name: String,
302 #[serde(default)]
304 pub trade: Option<String>,
305 #[serde(default)]
307 pub address_line1: Option<String>,
308 #[serde(default)]
310 pub city: Option<String>,
311 #[serde(default)]
313 pub state_or_province: Option<String>,
314 #[serde(default)]
316 pub country: Option<String>,
317 #[serde(default)]
319 pub member_count: Option<usize>,
320}
321
322#[derive(Debug, Clone, Deserialize, Serialize)]
328#[serde(rename_all = "camelCase")]
329pub struct FolderPermission {
330 pub id: String,
332 pub subject_id: String,
334 pub subject_type: SubjectType,
336 pub actions: Vec<String>,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
342#[serde(rename_all = "lowercase")]
343pub enum SubjectType {
344 User,
346 Role,
348 Company,
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_paginated_response_has_more() {
358 let response: PaginatedResponse<String> = PaginatedResponse {
359 results: vec!["a".to_string(), "b".to_string()],
360 pagination: PaginationInfo {
361 limit: 2,
362 offset: 0,
363 total_results: 10,
364 },
365 };
366 assert!(response.has_more());
367 assert_eq!(response.next_offset(), 2);
368 }
369
370 #[test]
371 fn test_paginated_response_last_page() {
372 let response: PaginatedResponse<String> = PaginatedResponse {
373 results: vec!["a".to_string()],
374 pagination: PaginationInfo {
375 limit: 2,
376 offset: 8,
377 total_results: 9,
378 },
379 };
380 assert!(!response.has_more());
381 }
382
383 #[test]
384 fn test_account_project_is_acc() {
385 let project = AccountProject {
386 id: "b.123".to_string(),
387 name: "Test".to_string(),
388 platform: Some("ACC".to_string()),
389 status: None,
390 account_id: None,
391 created_at: None,
392 updated_at: None,
393 project_type: None,
394 classification: None,
395 member_count: None,
396 company_count: None,
397 products: None,
398 };
399 assert!(project.is_acc());
400 assert!(!project.is_bim360());
401 }
402
403 #[test]
404 fn test_account_project_is_bim360() {
405 let project = AccountProject {
406 id: "b.123".to_string(),
407 name: "Test".to_string(),
408 platform: Some("BIM 360".to_string()),
409 status: None,
410 account_id: None,
411 created_at: None,
412 updated_at: None,
413 project_type: None,
414 classification: None,
415 member_count: None,
416 company_count: None,
417 products: None,
418 };
419 assert!(!project.is_acc());
420 assert!(project.is_bim360());
421 }
422
423 #[test]
424 fn test_project_classification_display() {
425 assert_eq!(
426 format!("{}", ProjectClassification::Production),
427 "production"
428 );
429 assert_eq!(format!("{}", ProjectClassification::Template), "template");
430 assert_eq!(format!("{}", ProjectClassification::Component), "component");
431 assert_eq!(format!("{}", ProjectClassification::Sample), "sample");
432 }
433
434 #[test]
435 fn test_account_project_is_template() {
436 let project = AccountProject {
437 id: "b.123".to_string(),
438 name: "Template Project".to_string(),
439 platform: Some("ACC".to_string()),
440 classification: Some(ProjectClassification::Template),
441 status: None,
442 account_id: None,
443 created_at: None,
444 updated_at: None,
445 project_type: None,
446 member_count: None,
447 company_count: None,
448 products: None,
449 };
450 assert!(project.is_template());
451 }
452
453 #[test]
454 fn test_account_project_not_template() {
455 let project = AccountProject {
456 id: "b.123".to_string(),
457 name: "Real Project".to_string(),
458 platform: Some("ACC".to_string()),
459 classification: Some(ProjectClassification::Production),
460 status: None,
461 account_id: None,
462 created_at: None,
463 updated_at: None,
464 project_type: None,
465 member_count: None,
466 company_count: None,
467 products: None,
468 };
469 assert!(!project.is_template());
470 }
471
472 #[test]
473 fn test_account_project_enabled_products() {
474 let project = AccountProject {
475 id: "b.123".to_string(),
476 name: "Test".to_string(),
477 platform: None,
478 products: Some(vec![
479 serde_json::json!({"key": "docs", "access": "administrator"}),
480 serde_json::json!({"key": "build", "access": "member"}),
481 serde_json::json!({"key": "modelCoordination", "access": "member"}),
482 ]),
483 status: None,
484 account_id: None,
485 created_at: None,
486 updated_at: None,
487 project_type: None,
488 classification: None,
489 member_count: None,
490 company_count: None,
491 };
492 let enabled = project.enabled_products();
493 assert_eq!(enabled.len(), 3);
494 assert!(enabled.contains(&"docs".to_string()));
495 }
496
497 #[test]
498 fn test_account_project_no_products() {
499 let project = AccountProject {
500 id: "b.123".to_string(),
501 name: "Test".to_string(),
502 platform: None,
503 products: None,
504 status: None,
505 account_id: None,
506 created_at: None,
507 updated_at: None,
508 project_type: None,
509 classification: None,
510 member_count: None,
511 company_count: None,
512 };
513 assert!(project.enabled_products().is_empty());
514 }
515
516 #[test]
517 fn test_company_deserialization() {
518 let json = r#"{
519 "id": "comp-123",
520 "name": "Acme Corp",
521 "trade": "General Contractor",
522 "city": "Portland",
523 "country": "US",
524 "memberCount": 42
525 }"#;
526 let company: Company = serde_json::from_str(json).unwrap();
527 assert_eq!(company.id, "comp-123");
528 assert_eq!(company.name, "Acme Corp");
529 assert_eq!(company.trade.unwrap(), "General Contractor");
530 assert_eq!(company.member_count.unwrap(), 42);
531 }
532
533 #[test]
534 fn test_company_deserialization_minimal() {
535 let json = r#"{"id": "comp-456", "name": "Minimal Co"}"#;
536 let company: Company = serde_json::from_str(json).unwrap();
537 assert_eq!(company.id, "comp-456");
538 assert!(company.trade.is_none());
539 assert!(company.member_count.is_none());
540 }
541}