Skip to main content

qubit_fs/write/
write_options.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Write operation options.
9
10use crate::error::FsError;
11use crate::error::FsErrorKind;
12use crate::error::FsOperation;
13use crate::metadata::AtomicityRequirement;
14use crate::metadata::Checksum;
15use crate::metadata::DurabilityRequirement;
16use crate::metadata::FileSystemCapabilities;
17use crate::metadata::FileSystemCapability;
18use crate::metadata::NonSensitiveMetadata;
19use crate::metadata::UserMetadata;
20use crate::write::WriteDisposition;
21use crate::write::WritePrecondition;
22
23/// Options controlling a write operation.
24///
25/// # Examples
26/// ```rust
27/// use qubit_fs::write::WriteOptions;
28/// use qubit_fs::metadata::AtomicityRequirement;
29/// use qubit_fs::metadata::FileSystemCapabilities;
30/// let options = WriteOptions::default().with_atomicity(AtomicityRequirement::Required);
31/// assert!(options.validate_against(FileSystemCapabilities::new()).is_err());
32/// ```
33#[non_exhaustive]
34#[derive(Clone, Debug, PartialEq)]
35pub struct WriteOptions {
36    /// Whether missing parent directories should be created.
37    create_parent: bool,
38    /// How an existing destination is treated.
39    disposition: WriteDisposition,
40    /// Required atomicity of destination publication.
41    atomicity: AtomicityRequirement,
42    /// Required durability of completed publication.
43    durability: DurabilityRequirement,
44    /// Version precondition applied to the destination.
45    precondition: WritePrecondition,
46    /// Optional content type.
47    content_type: Option<String>,
48    /// User-defined metadata with validated non-sensitive structural keys.
49    user_metadata: NonSensitiveMetadata,
50    /// Optional expected content checksum.
51    checksum: Option<Checksum>,
52}
53
54impl Default for WriteOptions {
55    #[inline]
56    fn default() -> Self {
57        Self {
58            create_parent: false,
59            disposition: WriteDisposition::default(),
60            atomicity: AtomicityRequirement::default(),
61            durability: DurabilityRequirement::default(),
62            precondition: WritePrecondition::default(),
63            content_type: None,
64            user_metadata: NonSensitiveMetadata::new(),
65            checksum: None,
66        }
67    }
68}
69
70impl WriteOptions {
71    /// Returns a copy with parent creation replaced.
72    #[inline]
73    #[must_use]
74    pub const fn with_create_parent(mut self, create: bool) -> Self {
75        self.create_parent = create;
76        self
77    }
78
79    /// Returns whether missing parent directories are created.
80    #[inline]
81    #[must_use]
82    pub const fn create_parent(&self) -> bool {
83        self.create_parent
84    }
85
86    /// Returns a copy with the destination disposition replaced.
87    #[inline]
88    #[must_use]
89    pub const fn with_disposition(mut self, disposition: WriteDisposition) -> Self {
90        self.disposition = disposition;
91        self
92    }
93
94    /// Returns the destination disposition.
95    #[inline]
96    #[must_use]
97    pub const fn disposition(&self) -> WriteDisposition {
98        self.disposition
99    }
100
101    /// Returns a copy with the atomicity requirement replaced.
102    #[inline]
103    #[must_use]
104    pub const fn with_atomicity(mut self, atomicity: AtomicityRequirement) -> Self {
105        self.atomicity = atomicity;
106        self
107    }
108
109    /// Returns the atomicity requirement.
110    #[inline]
111    #[must_use]
112    pub const fn atomicity(&self) -> AtomicityRequirement {
113        self.atomicity
114    }
115
116    /// Returns a copy with the durability requirement replaced.
117    #[inline]
118    #[must_use]
119    pub const fn with_durability(mut self, durability: DurabilityRequirement) -> Self {
120        self.durability = durability;
121        self
122    }
123
124    /// Returns the completed-publication durability requirement.
125    #[inline]
126    #[must_use]
127    pub const fn durability(&self) -> DurabilityRequirement {
128        self.durability
129    }
130
131    /// Returns a copy with the version precondition replaced.
132    #[inline]
133    #[must_use]
134    pub fn with_precondition(mut self, precondition: WritePrecondition) -> Self {
135        self.precondition = precondition;
136        self
137    }
138
139    /// Returns the version precondition.
140    #[inline]
141    #[must_use]
142    pub const fn precondition(&self) -> &WritePrecondition {
143        &self.precondition
144    }
145
146    /// Returns a copy with the content type replaced.
147    #[inline]
148    #[must_use]
149    pub fn with_content_type(mut self, content_type: Option<String>) -> Self {
150        self.content_type = content_type;
151        self
152    }
153
154    /// Returns the optional content type.
155    #[inline]
156    #[must_use]
157    pub fn content_type(&self) -> Option<&str> {
158        self.content_type.as_deref()
159    }
160
161    /// Returns the user metadata attached to this write.
162    #[inline]
163    #[must_use]
164    pub const fn user_metadata(&self) -> &NonSensitiveMetadata {
165        &self.user_metadata
166    }
167
168    /// Returns a copy with the expected checksum replaced.
169    #[inline]
170    #[must_use]
171    pub fn with_checksum(mut self, checksum: Option<Checksum>) -> Self {
172        self.checksum = checksum;
173        self
174    }
175
176    /// Returns the optional expected checksum.
177    #[inline]
178    #[must_use]
179    pub const fn checksum(&self) -> Option<&Checksum> {
180        self.checksum.as_ref()
181    }
182
183    /// Replaces user-defined metadata that has already passed key validation.
184    #[inline]
185    #[must_use]
186    pub fn with_user_metadata(mut self, metadata: UserMetadata) -> Self {
187        self.user_metadata = NonSensitiveMetadata::from(metadata);
188        self
189    }
190
191    /// Validates combinations that have no coherent provider interpretation.
192    ///
193    /// # Errors
194    /// Returns [`FsErrorKind::InvalidOptions`] before a writer is opened when
195    /// append is combined with atomic publication or a version precondition,
196    /// or when create-new is combined with an existing-version requirement.
197    #[inline]
198    pub fn validate(&self) -> Result<(), FsError> {
199        if self.disposition == WriteDisposition::Append
200            && (self.atomicity == AtomicityRequirement::Required || self.precondition != WritePrecondition::None)
201        {
202            return Err(FsError::new(
203                FsErrorKind::InvalidOptions,
204                FsOperation::OpenWriter,
205                "append cannot require atomic publication or a destination version",
206            ));
207        }
208        if self.disposition == WriteDisposition::CreateNew
209            && matches!(&self.precondition, WritePrecondition::IfMatch(_))
210        {
211            return Err(FsError::new(
212                FsErrorKind::InvalidOptions,
213                FsOperation::OpenWriter,
214                "create-new cannot require an existing destination version",
215            ));
216        }
217        Ok(())
218    }
219
220    /// Validates write options against stable configured capabilities.
221    ///
222    /// Providers should call this before opening a write session. In
223    /// particular, required atomic publication is rejected before any staging
224    /// write when [`FileSystemCapability::AtomicReplace`] is absent.
225    ///
226    /// # Errors
227    ///
228    /// Returns invalid-option errors from [`Self::validate`], or a typed
229    /// [`FsErrorKind::RequirementNotMet`] for unsupported append, conditional,
230    /// or required-atomic writes.
231    pub fn validate_against(&self, capabilities: FileSystemCapabilities) -> Result<(), FsError> {
232        self.validate()?;
233        if self.disposition == WriteDisposition::Append && !capabilities.supports(FileSystemCapability::Append) {
234            return Err(missing_requirement(
235                FileSystemCapability::Append,
236                "append writes are required but not supported",
237            ));
238        }
239        if self.precondition != WritePrecondition::None
240            && !capabilities.supports(FileSystemCapability::ConditionalWrite)
241        {
242            return Err(missing_requirement(
243                FileSystemCapability::ConditionalWrite,
244                "conditional writes are required but not supported",
245            ));
246        }
247        if self.atomicity == AtomicityRequirement::Required
248            && !capabilities.supports(FileSystemCapability::AtomicReplace)
249        {
250            return Err(missing_requirement(
251                FileSystemCapability::AtomicReplace,
252                "atomic write publication is required but not supported",
253            ));
254        }
255        if self.durability == DurabilityRequirement::Required
256            && !capabilities.supports(FileSystemCapability::DurableWrite)
257        {
258            return Err(missing_requirement(
259                FileSystemCapability::DurableWrite,
260                "durable write publication is required but not supported",
261            ));
262        }
263        Ok(())
264    }
265}
266
267/// Builds a typed unmet write requirement.
268fn missing_requirement(capability: FileSystemCapability, message: &str) -> FsError {
269    FsError::new(FsErrorKind::RequirementNotMet, FsOperation::OpenWriter, message).with_required_capability(capability)
270}
271
272#[cfg(test)]
273mod tests {
274    use std::hint::black_box;
275
276    use super::WriteOptions;
277    use crate::metadata::Checksum;
278    use crate::metadata::ChecksumAlgorithm;
279    use crate::metadata::NonSensitiveMetadata;
280    use crate::write::WriteDisposition;
281    use crate::write::WritePrecondition;
282
283    #[test]
284    fn option_accessors_are_executed_at_runtime() {
285        let disposition: fn(&WriteOptions) -> WriteDisposition = black_box(WriteOptions::disposition);
286        let precondition: for<'a> fn(&'a WriteOptions) -> &'a WritePrecondition = black_box(WriteOptions::precondition);
287        let create_parent: fn(&WriteOptions) -> bool = black_box(WriteOptions::create_parent);
288        let user_metadata: fn(&WriteOptions) -> &NonSensitiveMetadata = black_box(WriteOptions::user_metadata);
289        let checksum: for<'a> fn(&'a WriteOptions) -> Option<&'a Checksum> = black_box(WriteOptions::checksum);
290
291        let options = WriteOptions::default()
292            .with_create_parent(true)
293            .with_disposition(WriteDisposition::CreateOrReplace)
294            .with_checksum(Some(Checksum::new(ChecksumAlgorithm::Sha256, "abc")));
295
296        assert_eq!(WriteDisposition::CreateOrReplace, disposition(&options));
297        assert!(matches!(precondition(&options), WritePrecondition::None));
298        assert!(create_parent(&options));
299        assert!(user_metadata(&options).is_empty());
300        assert!(checksum(&options).is_some());
301    }
302}