haste_document_storage/
types.rs1use std::{
2 collections::BTreeMap,
3 fmt::{self, Debug, Display},
4 pin::Pin,
5 str::FromStr,
6 task::{Context, Poll},
7};
8
9use bytes::{Bytes, BytesMut};
10use futures_core::Stream;
11use futures_util::StreamExt as _;
12use time::OffsetDateTime;
13
14use crate::error::StorageError;
15
16const MAX_KEY_BYTES: usize = 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct DocumentKey(String);
27
28impl DocumentKey {
29 pub fn new(key: impl Into<String>) -> Result<Self, StorageError> {
35 let key = key.into();
36 let problem = if key.is_empty() {
37 Some("must not be empty")
38 } else if key.len() > MAX_KEY_BYTES {
39 Some("must be at most 1024 bytes")
40 } else if key.starts_with('/') {
41 Some("must not start with a slash")
42 } else if key.chars().any(char::is_control) {
43 Some("must not contain control characters")
44 } else if key
45 .split('/')
46 .any(|segment| matches!(segment, "" | "." | ".."))
47 {
48 Some("must not contain empty, '.' or '..' segments")
49 } else {
50 None
51 };
52
53 match problem {
54 Some(problem) => Err(StorageError::InvalidKey(format!("'{key}' {problem}"))),
55 None => Ok(Self(key)),
56 }
57 }
58
59 #[must_use]
62 pub fn from_backend(name: String) -> Self {
63 Self(name)
64 }
65
66 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70}
71
72impl Display for DocumentKey {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(&self.0)
75 }
76}
77
78impl AsRef<str> for DocumentKey {
79 fn as_ref(&self) -> &str {
80 &self.0
81 }
82}
83
84impl FromStr for DocumentKey {
85 type Err = StorageError;
86
87 fn from_str(key: &str) -> Result<Self, Self::Err> {
88 Self::new(key)
89 }
90}
91
92impl From<DocumentKey> for String {
93 fn from(key: DocumentKey) -> Self {
94 key.0
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct Document {
101 pub body: Bytes,
102 pub content_type: Option<String>,
105 pub metadata: BTreeMap<String, String>,
108}
109
110impl Document {
111 #[must_use]
112 pub fn new(body: impl Into<Bytes>) -> Self {
113 Self {
114 body: body.into(),
115 ..Self::default()
116 }
117 }
118
119 #[must_use]
120 pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
121 self.content_type = Some(content_type.into());
122 self
123 }
124
125 #[must_use]
126 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
127 self.metadata.insert(key.into(), value.into());
128 self
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct DocumentMetadata {
135 pub key: DocumentKey,
136 pub size: u64,
138 pub content_type: Option<String>,
139 pub etag: Option<String>,
141 pub last_modified: Option<OffsetDateTime>,
142 pub metadata: BTreeMap<String, String>,
144}
145
146pub struct DocumentBody(Pin<Box<dyn Stream<Item = Result<Bytes, StorageError>> + Send>>);
149
150impl DocumentBody {
151 pub fn from_stream<S>(stream: S) -> Self
152 where
153 S: Stream<Item = Result<Bytes, StorageError>> + Send + 'static,
154 {
155 Self(Box::pin(stream))
156 }
157
158 pub async fn collect(mut self) -> Result<Bytes, StorageError> {
165 let mut buffer = BytesMut::new();
166 while let Some(chunk) = self.0.next().await {
167 buffer.extend_from_slice(&chunk?);
168 }
169 Ok(buffer.freeze())
170 }
171}
172
173impl Stream for DocumentBody {
174 type Item = Result<Bytes, StorageError>;
175
176 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
177 self.0.as_mut().poll_next(cx)
178 }
179}
180
181impl Debug for DocumentBody {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.write_str("DocumentBody(..)")
184 }
185}
186
187#[derive(Debug)]
189pub struct StoredDocument {
190 pub metadata: DocumentMetadata,
191 pub body: DocumentBody,
192}
193
194#[derive(Debug, Clone, Default)]
196pub struct ListRequest {
197 pub prefix: Option<String>,
199 pub page_token: Option<String>,
201 pub page_size: Option<u32>,
203}
204
205#[derive(Debug, Clone)]
207pub struct ListPage {
208 pub documents: Vec<DocumentMetadata>,
209 pub next_page_token: Option<String>,
211}