docbox_http/models/
document_box.rs1use std::{fmt::Display, str::FromStr};
2
3use crate::error::HttpError;
4use axum::http::StatusCode;
5use docbox_core::database::models::{
6 document_box::DocumentBox,
7 folder::{FolderWithExtra, ResolvedFolderWithExtra},
8};
9use garde::Validate;
10use serde::{Deserialize, Deserializer, Serialize};
11use thiserror::Error;
12use utoipa::ToSchema;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, Serialize)]
16#[serde(transparent)]
17#[schema(examples( "user:1:files"), value_type = String)]
18pub struct DocumentBoxScope(pub String);
19
20impl Display for DocumentBoxScope {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 self.0.fmt(f)
23 }
24}
25
26const ALLOWED_CHARS: [char; 4] = [':', '-', '_', '.'];
27
28impl DocumentBoxScope {
29 pub fn validate_scope(value: &str) -> bool {
30 if value.trim().is_empty() {
31 return false;
32 }
33
34 value
35 .chars()
36 .all(|char| char.is_ascii_alphanumeric() || ALLOWED_CHARS.contains(&char))
37 }
38}
39
40impl<'de> Deserialize<'de> for DocumentBoxScope {
41 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42 where
43 D: Deserializer<'de>,
44 {
45 let value = String::deserialize(deserializer)?;
46
47 if !DocumentBoxScope::validate_scope(&value) {
48 return Err(serde::de::Error::custom(InvalidDocumentBoxScope));
49 }
50
51 Ok(Self(value))
52 }
53}
54
55impl Validate for DocumentBoxScope {
56 type Context = ();
57
58 fn validate_into(
59 &self,
60 _ctx: &Self::Context,
61 parent: &mut dyn FnMut() -> garde::Path,
62 report: &mut garde::Report,
63 ) {
64 if !DocumentBoxScope::validate_scope(&self.0) {
65 report.append(parent(), garde::Error::new("document box scope is invalid"))
66 }
67 }
68}
69
70#[derive(Debug, Error)]
71#[error("invalid document box scope")]
72pub struct InvalidDocumentBoxScope;
73
74impl FromStr for DocumentBoxScope {
75 type Err = InvalidDocumentBoxScope;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 if !DocumentBoxScope::validate_scope(s) {
79 return Err(InvalidDocumentBoxScope);
80 }
81
82 Ok(Self(s.to_string()))
83 }
84}
85
86#[derive(Debug, Validate, Deserialize, ToSchema)]
88pub struct CreateDocumentBoxRequest {
89 #[garde(length(min = 1))]
91 #[schema(min_length = 1)]
92 pub scope: String,
93}
94
95#[derive(Debug, Serialize, ToSchema)]
97pub struct DocumentBoxOptions {
98 pub max_file_size: i32,
100}
101
102#[derive(Debug, Serialize, ToSchema)]
104pub struct DocumentBoxResponse {
105 pub document_box: DocumentBox,
107 pub root: FolderWithExtra,
109 pub children: ResolvedFolderWithExtra,
111}
112
113#[derive(Debug, Serialize, ToSchema)]
114pub struct DocumentBoxStats {
115 pub total_files: i64,
117 pub total_links: i64,
119 pub total_folders: i64,
121 pub file_size: i64,
123}
124
125#[derive(Debug, Error)]
126pub enum HttpDocumentBoxError {
127 #[error("document box with matching scope already exists")]
128 ScopeAlreadyExists,
129
130 #[error("unknown document box")]
131 UnknownDocumentBox,
132}
133
134impl HttpError for HttpDocumentBoxError {
135 fn status(&self) -> axum::http::StatusCode {
136 match self {
137 HttpDocumentBoxError::ScopeAlreadyExists => StatusCode::CONFLICT,
138 HttpDocumentBoxError::UnknownDocumentBox => StatusCode::NOT_FOUND,
139 }
140 }
141}