Skip to main content

opendal_core/raw/
ops.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Ops provides the operation args struct like [`OpRead`] for user.
19//!
20//! By using ops, users can add more context for operation.
21
22use crate::BytesRange;
23use crate::UserMetadata;
24use crate::options;
25use crate::raw::*;
26use crate::types::compact::CompactValues;
27use crate::types::metadata::user_metadata_encoded_len;
28use crate::types::metadata::write_user_metadata;
29use crate::{Capability, Error, ErrorKind, Result};
30
31use std::collections::HashMap;
32
33#[inline]
34fn string_value(values: &CompactValues, field: usize) -> Option<&str> {
35    values.get_str(field)
36}
37
38#[inline]
39fn encode_timestamp(value: Timestamp) -> [u8; 12] {
40    let value = value.into_inner();
41    let mut encoded = [0; 12];
42    encoded[..8].copy_from_slice(&value.as_second().to_le_bytes());
43    encoded[8..].copy_from_slice(&value.subsec_nanosecond().to_le_bytes());
44    encoded
45}
46
47#[inline]
48fn decode_timestamp(value: &[u8]) -> Timestamp {
49    let seconds = i64::from_le_bytes(value[..8].try_into().unwrap());
50    let nanoseconds = i32::from_le_bytes(value[8..].try_into().unwrap());
51    Timestamp::new(seconds, nanoseconds).expect("operation stores a previously validated timestamp")
52}
53
54fn sorted_user_metadata(value: HashMap<String, String>) -> Vec<(String, String)> {
55    let mut value: Vec<_> = value.into_iter().collect();
56    value.sort_unstable_by(|left, right| left.0.cmp(&right.0));
57    value
58}
59
60/// Arguments for `create` operation.
61///
62/// The path must be normalized.
63#[derive(Debug, Clone, Default)]
64pub struct OpCreateDir {}
65
66impl OpCreateDir {
67    /// Create a new `OpCreateDir`.
68    pub fn new() -> Self {
69        Self::default()
70    }
71}
72
73/// Arguments for `delete` operation.
74///
75/// The path must be normalized.
76#[derive(Debug, Clone, Default, Eq, Hash, PartialEq)]
77pub struct OpDelete {
78    flags: u8,
79    values: CompactValues,
80}
81
82const OP_DELETE_RECURSIVE: u8 = 1;
83
84#[repr(usize)]
85enum DeleteField {
86    Version,
87    IfMatch,
88    IfNoneMatch,
89    IfVersionMatch,
90    IfVersionNotMatch,
91}
92
93impl OpDelete {
94    /// Create a new `OpDelete`.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Lower `options` against `capability` and freeze them into service-ready arguments.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error when `if_not_changed` contains no identity or conflicts
104    /// with an explicit equality condition.
105    ///
106    /// # Panics
107    ///
108    /// Panics when the compact value block, including its index, exceeds `u16::MAX` bytes.
109    pub fn from_options(
110        capability: &Capability,
111        mut options: options::DeleteOptions,
112    ) -> Result<Self> {
113        if let Some(metadata) = options.if_not_changed.take() {
114            let (target, identity, name) = if capability.delete_with_if_version_match
115                && let Some(version) = metadata.version()
116            {
117                (&mut options.if_version_match, version, "if_version_match")
118            } else if let Some(etag) = metadata.etag() {
119                (&mut options.if_match, etag, "if_match")
120            } else if let Some(version) = metadata.version() {
121                (&mut options.if_version_match, version, "if_version_match")
122            } else {
123                return Err(Error::new(
124                    ErrorKind::ConfigInvalid,
125                    "if_not_changed metadata contains neither version nor ETag",
126                )
127                .with_operation(Operation::Delete));
128            };
129
130            if let Some(explicit) = target {
131                if explicit != identity {
132                    return Err(Error::new(
133                        ErrorKind::ConditionNotMatch,
134                        format!("if_not_changed conflicts with {name}"),
135                    )
136                    .with_operation(Operation::Delete));
137                }
138            } else {
139                *target = Some(identity.to_owned());
140            }
141        }
142
143        let fields = [
144            options.version.as_deref().map(str::as_bytes),
145            options.if_match.as_deref().map(str::as_bytes),
146            options.if_none_match.as_deref().map(str::as_bytes),
147            options.if_version_match.as_deref().map(str::as_bytes),
148            options.if_version_not_match.as_deref().map(str::as_bytes),
149        ];
150        Ok(Self {
151            flags: if options.recursive {
152                OP_DELETE_RECURSIVE
153            } else {
154                0
155            },
156            values: CompactValues::encode(&fields),
157        })
158    }
159}
160
161impl OpDelete {
162    /// Return the version of the object to delete.
163    #[inline]
164    pub fn version(&self) -> Option<&str> {
165        string_value(&self.values, DeleteField::Version as usize)
166    }
167
168    /// Whether this delete should remove objects recursively.
169    #[inline]
170    pub fn recursive(&self) -> bool {
171        self.flags & OP_DELETE_RECURSIVE != 0
172    }
173
174    /// Return the ETag that the object must match before deletion.
175    #[inline]
176    pub fn if_match(&self) -> Option<&str> {
177        string_value(&self.values, DeleteField::IfMatch as usize)
178    }
179
180    /// Return the ETag that the object must not match before deletion.
181    #[inline]
182    pub fn if_none_match(&self) -> Option<&str> {
183        string_value(&self.values, DeleteField::IfNoneMatch as usize)
184    }
185
186    /// Return the version that the current object must match before deletion.
187    #[inline]
188    pub fn if_version_match(&self) -> Option<&str> {
189        string_value(&self.values, DeleteField::IfVersionMatch as usize)
190    }
191
192    /// Return the version that the current object must not match before deletion.
193    #[inline]
194    pub fn if_version_not_match(&self) -> Option<&str> {
195        string_value(&self.values, DeleteField::IfVersionNotMatch as usize)
196    }
197
198    /// Returns whether at least one value-bearing conditional option is set.
199    #[inline]
200    pub fn is_conditional(&self) -> bool {
201        self.if_match().is_some()
202            || self.if_none_match().is_some()
203            || self.if_version_match().is_some()
204            || self.if_version_not_match().is_some()
205    }
206
207    pub(crate) fn set_recursive(&mut self, recursive: bool) {
208        if recursive {
209            self.flags |= OP_DELETE_RECURSIVE;
210        } else {
211            self.flags &= !OP_DELETE_RECURSIVE;
212        }
213    }
214
215    #[doc(hidden)]
216    pub fn into_version(mut self, version: &str) -> Self {
217        self.values = self
218            .values
219            .replace(DeleteField::Version as usize, version.as_bytes());
220        self
221    }
222}
223
224/// Arguments for `delete` operation.
225///
226/// The path must be normalized.
227#[derive(Debug, Clone, Default)]
228pub struct OpDeleter {}
229
230impl OpDeleter {
231    /// Create a new `OpDelete`.
232    pub fn new() -> Self {
233        Self::default()
234    }
235}
236
237/// Arguments for `list` operation.
238#[derive(Debug, Clone, Default)]
239pub struct OpList {
240    /// The maximum number of results that the service should return per request.
241    ///
242    /// This can be used to control the memory consumption of a list operation.
243    limit: usize,
244    flags: u8,
245    values: CompactValues,
246}
247
248const OP_LIST_HAS_LIMIT: u8 = 1;
249const OP_LIST_RECURSIVE: u8 = 1 << 1;
250const OP_LIST_VERSIONS: u8 = 1 << 2;
251const OP_LIST_DELETED: u8 = 1 << 3;
252
253impl OpList {
254    /// Create a new `OpList`.
255    pub fn new() -> Self {
256        Self::default()
257    }
258
259    /// Return the maximum number of results per request.
260    #[inline]
261    pub fn limit(&self) -> Option<usize> {
262        (self.flags & OP_LIST_HAS_LIMIT != 0).then_some(self.limit)
263    }
264
265    /// Return the key after which listing should start.
266    #[inline]
267    pub fn start_after(&self) -> Option<&str> {
268        string_value(&self.values, 0)
269    }
270
271    /// Return whether the list operation is recursive.
272    #[inline]
273    pub fn recursive(&self) -> bool {
274        self.flags & OP_LIST_RECURSIVE != 0
275    }
276
277    /// Get the concurrent of list operation.
278    #[deprecated(since = "0.53.2", note = "concurrent in list is no-op")]
279    #[inline]
280    pub fn concurrent(&self) -> usize {
281        0
282    }
283
284    /// Return whether the operation includes object versions.
285    #[inline]
286    pub fn versions(&self) -> bool {
287        self.flags & OP_LIST_VERSIONS != 0
288    }
289
290    /// Return whether the operation includes deleted objects.
291    #[inline]
292    pub fn deleted(&self) -> bool {
293        self.flags & OP_LIST_DELETED != 0
294    }
295}
296
297impl From<options::ListOptions> for OpList {
298    fn from(value: options::ListOptions) -> Self {
299        let mut flags = 0;
300        if value.limit.is_some() {
301            flags |= OP_LIST_HAS_LIMIT;
302        }
303        if value.recursive {
304            flags |= OP_LIST_RECURSIVE;
305        }
306        if value.versions {
307            flags |= OP_LIST_VERSIONS;
308        }
309        if value.deleted {
310            flags |= OP_LIST_DELETED;
311        }
312        Self {
313            limit: value.limit.unwrap_or_default(),
314            flags,
315            values: CompactValues::encode(&[value.start_after.as_deref().map(str::as_bytes)]),
316        }
317    }
318}
319
320/// Arguments for `presign` operation.
321///
322/// The path must be normalized.
323#[derive(Debug, Clone)]
324pub struct OpPresign {
325    expire: Duration,
326
327    op: PresignOperation,
328}
329
330impl OpPresign {
331    /// Create a new `OpPresign`.
332    pub fn new(op: impl Into<PresignOperation>, expire: Duration) -> Self {
333        Self {
334            op: op.into(),
335            expire,
336        }
337    }
338
339    /// Return the operation to presign.
340    pub fn operation(&self) -> &PresignOperation {
341        &self.op
342    }
343
344    /// Return the request expiration duration.
345    pub fn expire(&self) -> Duration {
346        self.expire
347    }
348
349    /// Consume OpPresign into (Duration, PresignOperation)
350    pub fn into_parts(self) -> (Duration, PresignOperation) {
351        (self.expire, self.op)
352    }
353}
354
355/// Presign operation used for presign.
356#[derive(Debug, Clone)]
357#[non_exhaustive]
358pub enum PresignOperation {
359    /// Presign a stat(head) operation.
360    Stat(OpStat),
361    /// Presign a read operation.
362    Read(BytesRange, OpRead),
363    /// Presign a write operation.
364    Write(OpWrite),
365    /// Presign a delete operation.
366    Delete(OpDelete),
367}
368
369impl From<OpStat> for PresignOperation {
370    fn from(op: OpStat) -> Self {
371        Self::Stat(op)
372    }
373}
374
375impl From<OpRead> for PresignOperation {
376    fn from(v: OpRead) -> Self {
377        Self::Read(BytesRange::default(), v)
378    }
379}
380
381impl From<OpWrite> for PresignOperation {
382    fn from(v: OpWrite) -> Self {
383        Self::Write(v)
384    }
385}
386
387impl From<OpDelete> for PresignOperation {
388    fn from(v: OpDelete) -> Self {
389        Self::Delete(v)
390    }
391}
392
393/// Arguments for `read` operation.
394#[derive(Debug, Clone, Default)]
395pub struct OpRead {
396    values: CompactValues,
397}
398
399#[repr(usize)]
400enum ReadField {
401    IfMatch,
402    IfNoneMatch,
403    IfVersionMatch,
404    IfVersionNotMatch,
405    IfModifiedSince,
406    IfUnmodifiedSince,
407    OverrideContentType,
408    OverrideCacheControl,
409    OverrideContentDisposition,
410    Version,
411    ContentLengthHint,
412}
413
414impl OpRead {
415    /// Create a default `OpRead` which will read whole content of path.
416    pub fn new() -> Self {
417        Self::default()
418    }
419
420    /// Lower compose source `options` against `capability` and freeze them into
421    /// service-ready read arguments.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error when `if_not_changed` contains no identity or conflicts
426    /// with an explicit source equality condition.
427    ///
428    /// # Panics
429    ///
430    /// Panics when the compact value block, including its index, exceeds `u16::MAX` bytes.
431    pub(crate) fn from_compose_source_options(
432        capability: &Capability,
433        mut options: options::ComposeSourceOptions,
434    ) -> Result<Self> {
435        if let Some(metadata) = options.if_not_changed.take() {
436            let (target, identity, name) = if capability.compose_with_source_version
437                && let Some(version) = metadata.version()
438            {
439                (&mut options.version, version, "source version")
440            } else if let Some(etag) = metadata.etag() {
441                (&mut options.if_match, etag, "source if_match")
442            } else if let Some(version) = metadata.version() {
443                (&mut options.version, version, "source version")
444            } else {
445                return Err(Error::new(
446                    ErrorKind::ConfigInvalid,
447                    "if_not_changed metadata contains neither version nor ETag",
448                )
449                .with_operation(Operation::Compose));
450            };
451
452            if let Some(explicit) = target {
453                if explicit != identity {
454                    return Err(Error::new(
455                        ErrorKind::ConditionNotMatch,
456                        format!("if_not_changed conflicts with {name}"),
457                    )
458                    .with_operation(Operation::Compose));
459                }
460            } else {
461                *target = Some(identity.to_owned());
462            }
463        }
464
465        Ok(Self::from_read_options(options::ReadOptions {
466            version: options.version,
467            if_match: options.if_match,
468            ..Default::default()
469        }))
470    }
471
472    /// Returns the content-disposition header that should be sent back by the remote read
473    /// operation.
474    #[inline]
475    pub fn override_content_disposition(&self) -> Option<&str> {
476        string_value(&self.values, ReadField::OverrideContentDisposition as usize)
477    }
478
479    /// Returns the cache-control header that should be sent back by the remote read operation.
480    #[inline]
481    pub fn override_cache_control(&self) -> Option<&str> {
482        string_value(&self.values, ReadField::OverrideCacheControl as usize)
483    }
484
485    /// Returns the content-type header that should be sent back by the remote read operation.
486    #[inline]
487    pub fn override_content_type(&self) -> Option<&str> {
488        string_value(&self.values, ReadField::OverrideContentType as usize)
489    }
490
491    /// Get If-Match from option
492    #[inline]
493    pub fn if_match(&self) -> Option<&str> {
494        string_value(&self.values, ReadField::IfMatch as usize)
495    }
496
497    /// Get If-None-Match from option
498    #[inline]
499    pub fn if_none_match(&self) -> Option<&str> {
500        string_value(&self.values, ReadField::IfNoneMatch as usize)
501    }
502
503    /// Get the version match condition.
504    #[inline]
505    pub fn if_version_match(&self) -> Option<&str> {
506        string_value(&self.values, ReadField::IfVersionMatch as usize)
507    }
508
509    /// Get the version non-match condition.
510    #[inline]
511    pub fn if_version_not_match(&self) -> Option<&str> {
512        string_value(&self.values, ReadField::IfVersionNotMatch as usize)
513    }
514
515    /// Return the If-Modified-Since condition.
516    #[inline]
517    pub fn if_modified_since(&self) -> Option<Timestamp> {
518        self.values
519            .get(ReadField::IfModifiedSince as usize)
520            .map(decode_timestamp)
521    }
522
523    /// Get If-Unmodified-Since from option
524    #[inline]
525    pub fn if_unmodified_since(&self) -> Option<Timestamp> {
526        self.values
527            .get(ReadField::IfUnmodifiedSince as usize)
528            .map(decode_timestamp)
529    }
530
531    /// Returns whether at least one value-bearing conditional option is set.
532    #[inline]
533    pub fn is_conditional(&self) -> bool {
534        self.if_match().is_some()
535            || self.if_none_match().is_some()
536            || self.if_version_match().is_some()
537            || self.if_version_not_match().is_some()
538            || self.if_modified_since().is_some()
539            || self.if_unmodified_since().is_some()
540    }
541
542    /// Get version from option
543    #[inline]
544    pub fn version(&self) -> Option<&str> {
545        string_value(&self.values, ReadField::Version as usize)
546    }
547
548    pub(crate) fn content_length_hint(&self) -> Option<u64> {
549        self.values
550            .get(ReadField::ContentLengthHint as usize)
551            .map(|value| u64::from_le_bytes(value.try_into().unwrap()))
552    }
553
554    fn from_read_options(value: options::ReadOptions) -> Self {
555        let if_modified_since = value.if_modified_since.map(encode_timestamp);
556        let if_unmodified_since = value.if_unmodified_since.map(encode_timestamp);
557        let content_length_hint = value.content_length_hint.map(u64::to_le_bytes);
558        Self {
559            values: CompactValues::encode(&[
560                value.if_match.as_deref().map(str::as_bytes),
561                value.if_none_match.as_deref().map(str::as_bytes),
562                value.if_version_match.as_deref().map(str::as_bytes),
563                value.if_version_not_match.as_deref().map(str::as_bytes),
564                if_modified_since.as_ref().map(|value| value.as_slice()),
565                if_unmodified_since.as_ref().map(|value| value.as_slice()),
566                value.override_content_type.as_deref().map(str::as_bytes),
567                value.override_cache_control.as_deref().map(str::as_bytes),
568                value
569                    .override_content_disposition
570                    .as_deref()
571                    .map(str::as_bytes),
572                value.version.as_deref().map(str::as_bytes),
573                content_length_hint.as_ref().map(|value| value.as_slice()),
574            ]),
575        }
576    }
577}
578
579/// Arguments for reader operation.
580#[derive(Debug, Clone)]
581pub struct OpReader {
582    /// The number of concurrent requests that reader can send.
583    concurrent: usize,
584    /// Request chunk size.
585    chunk: Option<usize>,
586    /// The gap size of each request.
587    gap: Option<usize>,
588    /// The maximum number of buffers that can be prefetched.
589    prefetch: usize,
590}
591
592impl Default for OpReader {
593    fn default() -> Self {
594        Self {
595            concurrent: 1,
596            chunk: None,
597            gap: None,
598            prefetch: 0,
599        }
600    }
601}
602
603impl OpReader {
604    /// Create a new `OpReader`.
605    pub fn new() -> Self {
606        Self::default()
607    }
608
609    /// Set the number of concurrent requests the reader can send.
610    pub fn with_concurrent(mut self, concurrent: usize) -> Self {
611        self.concurrent = concurrent.max(1);
612        self
613    }
614
615    /// Return the number of concurrent requests.
616    pub fn concurrent(&self) -> usize {
617        self.concurrent
618    }
619
620    /// Set the request chunk size.
621    pub fn with_chunk(mut self, chunk: usize) -> Self {
622        self.chunk = Some(chunk.max(1));
623        self
624    }
625
626    /// Return the request chunk size.
627    pub fn chunk(&self) -> Option<usize> {
628        self.chunk
629    }
630
631    /// Set the gap size.
632    ///
633    /// Set to `0` to disable merging ranges separated by a gap. Overlapping or
634    /// adjacent ranges are still merged.
635    pub fn with_gap(mut self, gap: usize) -> Self {
636        self.gap = Some(gap);
637        self
638    }
639
640    /// Return the gap size.
641    pub fn gap(&self) -> Option<usize> {
642        self.gap
643    }
644
645    /// Set the number of prefetch requests.
646    pub fn with_prefetch(mut self, prefetch: usize) -> Self {
647        self.prefetch = prefetch;
648        self
649    }
650
651    /// Return the number of prefetch requests.
652    pub fn prefetch(&self) -> usize {
653        self.prefetch
654    }
655}
656
657impl From<options::ReadOptions> for (BytesRange, OpRead, OpReader) {
658    fn from(value: options::ReadOptions) -> Self {
659        let range = value.range;
660        let reader = OpReader {
661            concurrent: value.concurrent.max(1),
662            chunk: value.chunk,
663            gap: value.gap,
664            prefetch: 0,
665        };
666        (range, OpRead::from_read_options(value), reader)
667    }
668}
669
670impl From<options::ReaderOptions> for (OpRead, OpReader) {
671    fn from(value: options::ReaderOptions) -> Self {
672        let reader = OpReader {
673            concurrent: value.concurrent.max(1),
674            chunk: value.chunk,
675            gap: value.gap,
676            prefetch: value.prefetch,
677        };
678        let read = options::ReadOptions {
679            version: value.version,
680            if_match: value.if_match,
681            if_none_match: value.if_none_match,
682            if_version_match: value.if_version_match,
683            if_version_not_match: value.if_version_not_match,
684            if_modified_since: value.if_modified_since,
685            if_unmodified_since: value.if_unmodified_since,
686            content_length_hint: value.content_length_hint,
687            ..Default::default()
688        };
689        (OpRead::from_read_options(read), reader)
690    }
691}
692
693/// Arguments for `stat` operation.
694#[derive(Debug, Clone, Default)]
695pub struct OpStat {
696    values: CompactValues,
697}
698
699impl OpStat {
700    /// Create a new `OpStat`.
701    pub fn new() -> Self {
702        Self::default()
703    }
704
705    /// Get If-Match from option
706    #[inline]
707    pub fn if_match(&self) -> Option<&str> {
708        string_value(&self.values, ReadField::IfMatch as usize)
709    }
710
711    /// Get If-None-Match from option
712    #[inline]
713    pub fn if_none_match(&self) -> Option<&str> {
714        string_value(&self.values, ReadField::IfNoneMatch as usize)
715    }
716
717    /// Get the version match condition.
718    #[inline]
719    pub fn if_version_match(&self) -> Option<&str> {
720        string_value(&self.values, ReadField::IfVersionMatch as usize)
721    }
722
723    /// Get the version non-match condition.
724    #[inline]
725    pub fn if_version_not_match(&self) -> Option<&str> {
726        string_value(&self.values, ReadField::IfVersionNotMatch as usize)
727    }
728
729    /// Get If-Modified-Since from option
730    #[inline]
731    pub fn if_modified_since(&self) -> Option<Timestamp> {
732        self.values
733            .get(ReadField::IfModifiedSince as usize)
734            .map(decode_timestamp)
735    }
736
737    /// Get If-Unmodified-Since from option
738    #[inline]
739    pub fn if_unmodified_since(&self) -> Option<Timestamp> {
740        self.values
741            .get(ReadField::IfUnmodifiedSince as usize)
742            .map(decode_timestamp)
743    }
744
745    /// Returns whether at least one value-bearing conditional option is set.
746    #[inline]
747    pub fn is_conditional(&self) -> bool {
748        self.if_match().is_some()
749            || self.if_none_match().is_some()
750            || self.if_version_match().is_some()
751            || self.if_version_not_match().is_some()
752            || self.if_modified_since().is_some()
753            || self.if_unmodified_since().is_some()
754    }
755
756    /// Returns the content-disposition header that should be sent back by the remote read
757    /// operation.
758    #[inline]
759    pub fn override_content_disposition(&self) -> Option<&str> {
760        string_value(&self.values, ReadField::OverrideContentDisposition as usize)
761    }
762
763    /// Returns the cache-control header that should be sent back by the remote read operation.
764    #[inline]
765    pub fn override_cache_control(&self) -> Option<&str> {
766        string_value(&self.values, ReadField::OverrideCacheControl as usize)
767    }
768
769    /// Returns the content-type header that should be sent back by the remote read operation.
770    #[inline]
771    pub fn override_content_type(&self) -> Option<&str> {
772        string_value(&self.values, ReadField::OverrideContentType as usize)
773    }
774
775    /// Get version from option
776    #[inline]
777    pub fn version(&self) -> Option<&str> {
778        string_value(&self.values, ReadField::Version as usize)
779    }
780
781    pub(crate) fn from_read(value: &OpRead) -> Self {
782        // `OpStat` uses the same `ReadField` layout and simply ignores the
783        // read-only content-length hint.
784        Self {
785            values: value.values.clone(),
786        }
787    }
788}
789
790impl From<options::StatOptions> for OpStat {
791    fn from(value: options::StatOptions) -> Self {
792        let if_modified_since = value.if_modified_since.map(encode_timestamp);
793        let if_unmodified_since = value.if_unmodified_since.map(encode_timestamp);
794        Self {
795            values: CompactValues::encode(&[
796                value.if_match.as_deref().map(str::as_bytes),
797                value.if_none_match.as_deref().map(str::as_bytes),
798                value.if_version_match.as_deref().map(str::as_bytes),
799                value.if_version_not_match.as_deref().map(str::as_bytes),
800                if_modified_since.as_ref().map(|value| value.as_slice()),
801                if_unmodified_since.as_ref().map(|value| value.as_slice()),
802                value.override_content_type.as_deref().map(str::as_bytes),
803                value.override_cache_control.as_deref().map(str::as_bytes),
804                value
805                    .override_content_disposition
806                    .as_deref()
807                    .map(str::as_bytes),
808                value.version.as_deref().map(str::as_bytes),
809            ]),
810        }
811    }
812}
813
814/// Arguments for `write` operation.
815#[derive(Debug, Clone, Default)]
816pub struct OpWrite {
817    concurrent: usize,
818    flags: u8,
819    values: CompactValues,
820}
821
822const OP_WRITE_APPEND: u8 = 1;
823const OP_WRITE_IF_NOT_EXISTS: u8 = 1 << 1;
824
825#[repr(usize)]
826enum WriteField {
827    ContentType,
828    ContentDisposition,
829    ContentEncoding,
830    CacheControl,
831    IfMatch,
832    IfNoneMatch,
833    IfVersionMatch,
834    IfVersionNotMatch,
835    UserMetadata,
836}
837
838impl OpWrite {
839    /// Create a new `OpWrite`.
840    ///
841    /// If input path is not a file path, an error will be returned.
842    pub fn new() -> Self {
843        Self::default()
844    }
845
846    /// Lower `options` against `capability` and freeze them into service-ready arguments.
847    ///
848    /// # Errors
849    ///
850    /// Returns an error when `if_not_changed` contains no identity or conflicts
851    /// with an explicit equality condition.
852    ///
853    /// # Panics
854    ///
855    /// Panics when the compact value block, including its index, exceeds `u16::MAX` bytes.
856    pub fn from_options(
857        capability: &Capability,
858        mut options: options::WriteOptions,
859    ) -> Result<(Self, OpWriter)> {
860        if let Some(metadata) = options.if_not_changed.take() {
861            let (target, identity, name) = if capability.write_with_if_version_match
862                && let Some(version) = metadata.version()
863            {
864                (&mut options.if_version_match, version, "if_version_match")
865            } else if let Some(etag) = metadata.etag() {
866                (&mut options.if_match, etag, "if_match")
867            } else if let Some(version) = metadata.version() {
868                (&mut options.if_version_match, version, "if_version_match")
869            } else {
870                return Err(Error::new(
871                    ErrorKind::ConfigInvalid,
872                    "if_not_changed metadata contains neither version nor ETag",
873                )
874                .with_operation(Operation::Write));
875            };
876
877            if let Some(explicit) = target {
878                if explicit != identity {
879                    return Err(Error::new(
880                        ErrorKind::ConditionNotMatch,
881                        format!("if_not_changed conflicts with {name}"),
882                    )
883                    .with_operation(Operation::Write));
884                }
885            } else {
886                *target = Some(identity.to_owned());
887            }
888        }
889
890        let writer = OpWriter {
891            chunk: options.chunk,
892        };
893        let user_metadata = options.user_metadata.take().map(sorted_user_metadata);
894        let mut flags = 0;
895        if options.append {
896            flags |= OP_WRITE_APPEND;
897        }
898        if options.if_not_exists {
899            flags |= OP_WRITE_IF_NOT_EXISTS;
900        }
901        let fields = [
902            options.content_type.as_deref().map(str::as_bytes),
903            options.content_disposition.as_deref().map(str::as_bytes),
904            options.content_encoding.as_deref().map(str::as_bytes),
905            options.cache_control.as_deref().map(str::as_bytes),
906            options.if_match.as_deref().map(str::as_bytes),
907            options.if_none_match.as_deref().map(str::as_bytes),
908            options.if_version_match.as_deref().map(str::as_bytes),
909            options.if_version_not_match.as_deref().map(str::as_bytes),
910            None,
911        ];
912        let mut lengths = fields.map(|value| value.map(<[u8]>::len));
913        lengths[WriteField::UserMetadata as usize] =
914            user_metadata.as_deref().map(user_metadata_encoded_len);
915        let args = Self {
916            concurrent: options.concurrent.max(1),
917            flags,
918            values: CompactValues::encode_with(&lengths, |field, output| match field {
919                field if field == WriteField::UserMetadata as usize => {
920                    write_user_metadata(
921                        user_metadata.as_deref().expect("present field has a value"),
922                        output,
923                    );
924                }
925                _ => {
926                    output.write(fields[field].expect("present field has a value"));
927                }
928            }),
929        };
930        Ok((args, writer))
931    }
932
933    /// Get the append from op.
934    ///
935    /// The append is the flag to indicate that this write operation is an append operation.
936    #[inline]
937    pub fn append(&self) -> bool {
938        self.flags & OP_WRITE_APPEND != 0
939    }
940
941    /// Get the content type from option
942    #[inline]
943    pub fn content_type(&self) -> Option<&str> {
944        string_value(&self.values, WriteField::ContentType as usize)
945    }
946
947    /// Get the content disposition from option
948    #[inline]
949    pub fn content_disposition(&self) -> Option<&str> {
950        string_value(&self.values, WriteField::ContentDisposition as usize)
951    }
952
953    /// Get the content encoding from option
954    #[inline]
955    pub fn content_encoding(&self) -> Option<&str> {
956        string_value(&self.values, WriteField::ContentEncoding as usize)
957    }
958
959    /// Get the cache control from option
960    #[inline]
961    pub fn cache_control(&self) -> Option<&str> {
962        string_value(&self.values, WriteField::CacheControl as usize)
963    }
964
965    /// Get the concurrent.
966    #[inline]
967    pub fn concurrent(&self) -> usize {
968        self.concurrent
969    }
970
971    /// Get If-Match from option
972    #[inline]
973    pub fn if_match(&self) -> Option<&str> {
974        string_value(&self.values, WriteField::IfMatch as usize)
975    }
976
977    /// Get If-None-Match from option
978    #[inline]
979    pub fn if_none_match(&self) -> Option<&str> {
980        string_value(&self.values, WriteField::IfNoneMatch as usize)
981    }
982
983    /// Get the version match condition.
984    #[inline]
985    pub fn if_version_match(&self) -> Option<&str> {
986        string_value(&self.values, WriteField::IfVersionMatch as usize)
987    }
988
989    /// Get the version non-match condition.
990    #[inline]
991    pub fn if_version_not_match(&self) -> Option<&str> {
992        string_value(&self.values, WriteField::IfVersionNotMatch as usize)
993    }
994
995    /// Get If-Not-Exist from option
996    #[inline]
997    pub fn if_not_exists(&self) -> bool {
998        self.flags & OP_WRITE_IF_NOT_EXISTS != 0
999    }
1000
1001    /// Returns whether at least one conditional option is set.
1002    ///
1003    /// Value-bearing options are set when present. Boolean options are set only
1004    /// when their value is `true`.
1005    #[inline]
1006    pub fn is_conditional(&self) -> bool {
1007        self.if_not_exists()
1008            || self.if_match().is_some()
1009            || self.if_none_match().is_some()
1010            || self.if_version_match().is_some()
1011            || self.if_version_not_match().is_some()
1012    }
1013
1014    /// Get the user defined metadata from the op
1015    #[inline]
1016    pub fn user_metadata(&self) -> Option<UserMetadata<'_>> {
1017        self.values
1018            .get(WriteField::UserMetadata as usize)
1019            .map(UserMetadata::new)
1020    }
1021
1022    #[doc(hidden)]
1023    pub fn into_content_type(mut self, value: &str) -> Self {
1024        self.values = self
1025            .values
1026            .replace(WriteField::ContentType as usize, value.as_bytes());
1027        self
1028    }
1029}
1030
1031/// Arguments for `writer` operation.
1032#[derive(Debug, Clone, Default)]
1033pub struct OpWriter {
1034    chunk: Option<usize>,
1035}
1036
1037impl OpWriter {
1038    /// Create a new `OpWriter`.
1039    pub fn new() -> Self {
1040        Self::default()
1041    }
1042
1043    /// Get the chunk from op.
1044    ///
1045    /// The chunk is used by service to decide the chunk size of the underlying writer.
1046    pub fn chunk(&self) -> Option<usize> {
1047        self.chunk
1048    }
1049
1050    /// Set the chunk of op.
1051    ///
1052    /// If chunk is set, the data will be chunked by the underlying writer.
1053    ///
1054    /// ## NOTE
1055    ///
1056    /// Service could have their own minimum chunk size while perform write
1057    /// operations like multipart uploads. So the chunk size may be larger than
1058    /// the given buffer size.
1059    pub fn with_chunk(mut self, chunk: usize) -> Self {
1060        self.chunk = Some(chunk);
1061        self
1062    }
1063}
1064
1065/// Arguments for `compose` operation.
1066#[derive(Debug, Clone, Default)]
1067pub struct OpCompose {
1068    concurrent: usize,
1069    flags: u8,
1070    values: CompactValues,
1071}
1072
1073const OP_COMPOSE_IF_NOT_EXISTS: u8 = 1;
1074
1075#[repr(usize)]
1076enum ComposeField {
1077    ContentType,
1078    ContentDisposition,
1079    ContentEncoding,
1080    CacheControl,
1081    IfMatch,
1082    IfNoneMatch,
1083    IfVersionMatch,
1084    IfVersionNotMatch,
1085    UserMetadata,
1086}
1087
1088impl OpCompose {
1089    /// Create a new `OpCompose`.
1090    pub fn new() -> Self {
1091        Self::default()
1092    }
1093
1094    /// Lower `options` against `capability` and freeze them into service-ready arguments.
1095    ///
1096    /// # Errors
1097    ///
1098    /// Returns an error when `if_not_changed` contains no identity or conflicts
1099    /// with an explicit equality condition.
1100    ///
1101    /// # Panics
1102    ///
1103    /// Panics when the compact value block, including its index, exceeds `u16::MAX` bytes.
1104    pub fn from_options(
1105        capability: &Capability,
1106        mut options: options::ComposeOptions,
1107    ) -> Result<Self> {
1108        if let Some(metadata) = options.if_not_changed.take() {
1109            let (target, identity, name) = if capability.compose_with_if_version_match
1110                && let Some(version) = metadata.version()
1111            {
1112                (&mut options.if_version_match, version, "if_version_match")
1113            } else if let Some(etag) = metadata.etag() {
1114                (&mut options.if_match, etag, "if_match")
1115            } else if let Some(version) = metadata.version() {
1116                (&mut options.if_version_match, version, "if_version_match")
1117            } else {
1118                return Err(Error::new(
1119                    ErrorKind::ConfigInvalid,
1120                    "if_not_changed metadata contains neither version nor ETag",
1121                )
1122                .with_operation(Operation::Compose));
1123            };
1124
1125            if let Some(explicit) = target {
1126                if explicit != identity {
1127                    return Err(Error::new(
1128                        ErrorKind::ConditionNotMatch,
1129                        format!("if_not_changed conflicts with {name}"),
1130                    )
1131                    .with_operation(Operation::Compose));
1132                }
1133            } else {
1134                *target = Some(identity.to_owned());
1135            }
1136        }
1137
1138        let user_metadata = options.user_metadata.take().map(sorted_user_metadata);
1139        let fields = [
1140            options.content_type.as_deref().map(str::as_bytes),
1141            options.content_disposition.as_deref().map(str::as_bytes),
1142            options.content_encoding.as_deref().map(str::as_bytes),
1143            options.cache_control.as_deref().map(str::as_bytes),
1144            options.if_match.as_deref().map(str::as_bytes),
1145            options.if_none_match.as_deref().map(str::as_bytes),
1146            options.if_version_match.as_deref().map(str::as_bytes),
1147            options.if_version_not_match.as_deref().map(str::as_bytes),
1148            None,
1149        ];
1150        let mut lengths = fields.map(|value| value.map(<[u8]>::len));
1151        lengths[ComposeField::UserMetadata as usize] =
1152            user_metadata.as_deref().map(user_metadata_encoded_len);
1153        Ok(Self {
1154            concurrent: options.concurrent.max(1),
1155            flags: if options.if_not_exists {
1156                OP_COMPOSE_IF_NOT_EXISTS
1157            } else {
1158                0
1159            },
1160            values: CompactValues::encode_with(&lengths, |field, output| match field {
1161                field if field == ComposeField::UserMetadata as usize => {
1162                    write_user_metadata(
1163                        user_metadata.as_deref().expect("present field has a value"),
1164                        output,
1165                    );
1166                }
1167                _ => {
1168                    output.write(fields[field].expect("present field has a value"));
1169                }
1170            }),
1171        })
1172    }
1173
1174    /// Get the maximum concurrent composition task count.
1175    pub fn concurrent(&self) -> usize {
1176        self.concurrent.max(1)
1177    }
1178
1179    /// Get destination Content-Type metadata.
1180    pub fn content_type(&self) -> Option<&str> {
1181        string_value(&self.values, ComposeField::ContentType as usize)
1182    }
1183
1184    /// Get destination Content-Disposition metadata.
1185    pub fn content_disposition(&self) -> Option<&str> {
1186        string_value(&self.values, ComposeField::ContentDisposition as usize)
1187    }
1188
1189    /// Get destination Content-Encoding metadata.
1190    pub fn content_encoding(&self) -> Option<&str> {
1191        string_value(&self.values, ComposeField::ContentEncoding as usize)
1192    }
1193
1194    /// Get destination Cache-Control metadata.
1195    pub fn cache_control(&self) -> Option<&str> {
1196        string_value(&self.values, ComposeField::CacheControl as usize)
1197    }
1198
1199    /// Get destination user metadata.
1200    pub fn user_metadata(&self) -> Option<UserMetadata<'_>> {
1201        self.values
1202            .get(ComposeField::UserMetadata as usize)
1203            .map(UserMetadata::new)
1204    }
1205
1206    /// Get the destination ETag match condition.
1207    pub fn if_match(&self) -> Option<&str> {
1208        string_value(&self.values, ComposeField::IfMatch as usize)
1209    }
1210
1211    /// Get the destination ETag non-match condition.
1212    pub fn if_none_match(&self) -> Option<&str> {
1213        string_value(&self.values, ComposeField::IfNoneMatch as usize)
1214    }
1215
1216    /// Get the destination version match condition.
1217    pub fn if_version_match(&self) -> Option<&str> {
1218        string_value(&self.values, ComposeField::IfVersionMatch as usize)
1219    }
1220
1221    /// Get the destination version non-match condition.
1222    pub fn if_version_not_match(&self) -> Option<&str> {
1223        string_value(&self.values, ComposeField::IfVersionNotMatch as usize)
1224    }
1225
1226    /// Get whether composition requires a missing destination.
1227    pub fn if_not_exists(&self) -> bool {
1228        self.flags & OP_COMPOSE_IF_NOT_EXISTS != 0
1229    }
1230
1231    /// Returns whether at least one conditional option is set.
1232    ///
1233    /// Value-bearing options are set when present. Boolean options are set only
1234    /// when their value is `true`.
1235    #[inline]
1236    pub fn is_conditional(&self) -> bool {
1237        self.if_not_exists()
1238            || self.if_match().is_some()
1239            || self.if_none_match().is_some()
1240            || self.if_version_match().is_some()
1241            || self.if_version_not_match().is_some()
1242    }
1243
1244    #[doc(hidden)]
1245    pub fn into_content_type(mut self, value: &str) -> Self {
1246        self.values = self
1247            .values
1248            .replace(ComposeField::ContentType as usize, value.as_bytes());
1249        self
1250    }
1251}
1252
1253/// Arguments for `copy` operation.
1254#[derive(Debug, Clone, Default)]
1255pub struct OpCopy {
1256    concurrent: usize,
1257    chunk: Option<usize>,
1258    source_content_length_hint: Option<u64>,
1259    flags: u8,
1260    values: CompactValues,
1261}
1262
1263const OP_COPY_IF_NOT_EXISTS: u8 = 1;
1264
1265#[repr(usize)]
1266enum CopyField {
1267    IfMatch,
1268    IfNoneMatch,
1269    IfVersionMatch,
1270    IfVersionNotMatch,
1271    SourceVersion,
1272}
1273
1274impl OpCopy {
1275    /// Create a new `OpCopy`.
1276    pub fn new() -> Self {
1277        Self::default()
1278    }
1279
1280    /// Lower `options` against `capability` and freeze them into service-ready arguments.
1281    ///
1282    /// # Errors
1283    ///
1284    /// Returns an error when `if_not_changed` contains no identity or conflicts
1285    /// with an explicit equality condition.
1286    ///
1287    /// # Panics
1288    ///
1289    /// Panics when the compact value block, including its index, exceeds `u16::MAX` bytes.
1290    pub fn from_options(
1291        capability: &Capability,
1292        mut options: options::CopyOptions,
1293    ) -> Result<Self> {
1294        if let Some(metadata) = options.if_not_changed.take() {
1295            let (target, identity, name) = if capability.copy_with_if_version_match
1296                && let Some(version) = metadata.version()
1297            {
1298                (&mut options.if_version_match, version, "if_version_match")
1299            } else if let Some(etag) = metadata.etag() {
1300                (&mut options.if_match, etag, "if_match")
1301            } else if let Some(version) = metadata.version() {
1302                (&mut options.if_version_match, version, "if_version_match")
1303            } else {
1304                return Err(Error::new(
1305                    ErrorKind::ConfigInvalid,
1306                    "if_not_changed metadata contains neither version nor ETag",
1307                )
1308                .with_operation(Operation::Copy));
1309            };
1310
1311            if let Some(explicit) = target {
1312                if explicit != identity {
1313                    return Err(Error::new(
1314                        ErrorKind::ConditionNotMatch,
1315                        format!("if_not_changed conflicts with {name}"),
1316                    )
1317                    .with_operation(Operation::Copy));
1318                }
1319            } else {
1320                *target = Some(identity.to_owned());
1321            }
1322        }
1323
1324        let fields = [
1325            options.if_match.as_deref().map(str::as_bytes),
1326            options.if_none_match.as_deref().map(str::as_bytes),
1327            options.if_version_match.as_deref().map(str::as_bytes),
1328            options.if_version_not_match.as_deref().map(str::as_bytes),
1329            options.source_version.as_deref().map(str::as_bytes),
1330        ];
1331        Ok(Self {
1332            concurrent: options.concurrent.max(1),
1333            chunk: options.chunk,
1334            source_content_length_hint: options.source_content_length_hint,
1335            flags: if options.if_not_exists {
1336                OP_COPY_IF_NOT_EXISTS
1337            } else {
1338                0
1339            },
1340            values: CompactValues::encode(&fields),
1341        })
1342    }
1343
1344    /// Get if_not_exists flag.
1345    #[inline]
1346    pub fn if_not_exists(&self) -> bool {
1347        self.flags & OP_COPY_IF_NOT_EXISTS != 0
1348    }
1349
1350    /// Get if_match condition.
1351    #[inline]
1352    pub fn if_match(&self) -> Option<&str> {
1353        string_value(&self.values, CopyField::IfMatch as usize)
1354    }
1355
1356    /// Get the destination ETag non-match condition.
1357    #[inline]
1358    pub fn if_none_match(&self) -> Option<&str> {
1359        string_value(&self.values, CopyField::IfNoneMatch as usize)
1360    }
1361
1362    /// Get the current destination version match condition.
1363    #[inline]
1364    pub fn if_version_match(&self) -> Option<&str> {
1365        string_value(&self.values, CopyField::IfVersionMatch as usize)
1366    }
1367
1368    /// Get the current destination version non-match condition.
1369    #[inline]
1370    pub fn if_version_not_match(&self) -> Option<&str> {
1371        string_value(&self.values, CopyField::IfVersionNotMatch as usize)
1372    }
1373
1374    /// Returns whether at least one conditional option is set.
1375    ///
1376    /// Value-bearing options are set when present. Boolean options are set only
1377    /// when their value is `true`.
1378    #[inline]
1379    pub fn is_conditional(&self) -> bool {
1380        self.if_not_exists()
1381            || self.if_match().is_some()
1382            || self.if_none_match().is_some()
1383            || self.if_version_match().is_some()
1384            || self.if_version_not_match().is_some()
1385    }
1386
1387    /// Get source version from the operation.
1388    #[inline]
1389    pub fn source_version(&self) -> Option<&str> {
1390        string_value(&self.values, CopyField::SourceVersion as usize)
1391    }
1392
1393    /// Get the concurrent tasks for the copy operation.
1394    pub fn concurrent(&self) -> usize {
1395        self.concurrent.max(1)
1396    }
1397
1398    /// Get the chunk size for the copy operation.
1399    pub fn chunk(&self) -> Option<usize> {
1400        self.chunk
1401    }
1402
1403    /// Get source content length hint from the copy operation.
1404    pub fn source_content_length_hint(&self) -> Option<u64> {
1405        self.source_content_length_hint
1406    }
1407}
1408
1409/// Arguments for `rename` operation.
1410#[derive(Debug, Clone, Default)]
1411pub struct OpRename {
1412    /// Whether the rename should fail when the destination already exists.
1413    ///
1414    /// If `true`, the rename succeeds only when the destination does not exist.
1415    /// If `false`, the rename uses OpenDAL's default overwrite behavior.
1416    if_not_exists: bool,
1417}
1418
1419impl OpRename {
1420    /// Create a new `OpRename`.
1421    pub fn new() -> Self {
1422        Self::default()
1423    }
1424
1425    /// Set whether the rename should fail when the destination already exists.
1426    ///
1427    /// If `true`, the rename succeeds only when the destination does not exist.
1428    /// If `false`, the rename uses OpenDAL's default overwrite behavior.
1429    ///
1430    /// ## Service Implementation
1431    ///
1432    /// Check [`crate::Capability::rename_with_if_not_exists`] before setting this to
1433    /// `true`. A service might return `ErrorKind::Unsupported` if it cannot
1434    /// enforce the condition.
1435    pub fn with_if_not_exists(mut self, if_not_exists: bool) -> Self {
1436        self.if_not_exists = if_not_exists;
1437        self
1438    }
1439
1440    /// Return whether the rename should fail when the destination already exists.
1441    pub fn if_not_exists(&self) -> bool {
1442        self.if_not_exists
1443    }
1444
1445    /// Returns whether the boolean conditional option is set to `true`.
1446    #[inline]
1447    pub fn is_conditional(&self) -> bool {
1448        self.if_not_exists()
1449    }
1450}
1451
1452impl From<options::RenameOptions> for OpRename {
1453    fn from(value: options::RenameOptions) -> Self {
1454        Self {
1455            if_not_exists: value.if_not_exists,
1456        }
1457    }
1458}
1459
1460/// Arguments for `restore` operation.
1461#[derive(Debug, Clone, Default)]
1462pub struct OpRestore {
1463    flags: u8,
1464    values: CompactValues,
1465}
1466
1467const OP_RESTORE_IF_NOT_EXISTS: u8 = 1;
1468
1469impl OpRestore {
1470    /// Create a new `OpRestore`.
1471    pub fn new() -> Self {
1472        Self::default()
1473    }
1474
1475    /// Return the version to restore.
1476    #[inline]
1477    pub fn version(&self) -> Option<&str> {
1478        string_value(&self.values, 0)
1479    }
1480
1481    /// Return whether the restore should fail if the path currently exists.
1482    #[inline]
1483    pub fn if_not_exists(&self) -> bool {
1484        self.flags & OP_RESTORE_IF_NOT_EXISTS != 0
1485    }
1486
1487    /// Returns whether the boolean conditional option is set to `true`.
1488    #[inline]
1489    pub fn is_conditional(&self) -> bool {
1490        self.if_not_exists()
1491    }
1492}
1493
1494impl From<options::RestoreOptions> for OpRestore {
1495    fn from(value: options::RestoreOptions) -> Self {
1496        Self {
1497            flags: if value.if_not_exists {
1498                OP_RESTORE_IF_NOT_EXISTS
1499            } else {
1500                0
1501            },
1502            values: CompactValues::encode(&[value.version.as_deref().map(str::as_bytes)]),
1503        }
1504    }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509    use super::*;
1510    use crate::Metadata;
1511    use crate::MetadataBuilder;
1512
1513    fn condition_metadata() -> Metadata {
1514        let mut metadata = MetadataBuilder::file(42);
1515        metadata
1516            .etag("etag")
1517            .version("version")
1518            .user_metadata([("owner".to_owned(), "opendal".to_owned())]);
1519        metadata.build()
1520    }
1521
1522    #[test]
1523    fn compact_operation_layouts() {
1524        assert_eq!(size_of::<OpRead>(), 16);
1525        assert_eq!(size_of::<OpStat>(), 16);
1526        assert_eq!(size_of::<OpWrite>(), 32);
1527        assert_eq!(size_of::<OpCompose>(), 32);
1528        assert_eq!(size_of::<OpDelete>(), 24);
1529        assert_eq!(size_of::<OpCopy>(), 64);
1530        assert_eq!(size_of::<OpList>(), 32);
1531        assert_eq!(size_of::<OpRestore>(), 24);
1532    }
1533
1534    #[test]
1535    fn read_options_roundtrip() {
1536        let modified = Timestamp::new(-1, -123).unwrap();
1537        let unmodified = Timestamp::new(2, 456).unwrap();
1538        let options = options::ReadOptions {
1539            version: Some("version".to_owned()),
1540            if_match: Some("etag".to_owned()),
1541            if_none_match: Some("other-etag".to_owned()),
1542            if_version_match: Some("version-match".to_owned()),
1543            if_version_not_match: Some("version-not-match".to_owned()),
1544            if_modified_since: Some(modified),
1545            if_unmodified_since: Some(unmodified),
1546            content_length_hint: Some(42),
1547            override_content_type: Some("text/plain".to_owned()),
1548            override_cache_control: Some("no-cache".to_owned()),
1549            override_content_disposition: Some("attachment".to_owned()),
1550            ..Default::default()
1551        };
1552        let (_, args, _) = options.into();
1553
1554        assert_eq!(args.version(), Some("version"));
1555        assert_eq!(args.if_match(), Some("etag"));
1556        assert_eq!(args.if_none_match(), Some("other-etag"));
1557        assert_eq!(args.if_version_match(), Some("version-match"));
1558        assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1559        assert_eq!(args.if_modified_since(), Some(modified));
1560        assert_eq!(args.if_unmodified_since(), Some(unmodified));
1561        assert_eq!(args.content_length_hint(), Some(42));
1562        assert_eq!(args.override_content_type(), Some("text/plain"));
1563        assert_eq!(args.override_cache_control(), Some("no-cache"));
1564        assert_eq!(args.override_content_disposition(), Some("attachment"));
1565        assert!(args.is_conditional());
1566    }
1567
1568    #[test]
1569    fn write_options_preserve_owned_views() {
1570        let (args, _) = OpWrite::from_options(
1571            &Capability::default(),
1572            options::WriteOptions {
1573                append: true,
1574                concurrent: 4,
1575                content_type: Some("text/plain".to_owned()),
1576                content_disposition: Some("attachment".to_owned()),
1577                content_encoding: Some("gzip".to_owned()),
1578                cache_control: Some("no-cache".to_owned()),
1579                if_match: Some("etag".to_owned()),
1580                if_none_match: Some("other-etag".to_owned()),
1581                if_version_match: Some("version-match".to_owned()),
1582                if_version_not_match: Some("version-not-match".to_owned()),
1583                if_not_exists: true,
1584                user_metadata: Some(HashMap::from([("owner".to_owned(), "opendal".to_owned())])),
1585                ..Default::default()
1586            },
1587        )
1588        .unwrap();
1589
1590        assert!(args.append());
1591        assert_eq!(args.concurrent(), 4);
1592        assert_eq!(args.content_type(), Some("text/plain"));
1593        assert_eq!(args.content_disposition(), Some("attachment"));
1594        assert_eq!(args.content_encoding(), Some("gzip"));
1595        assert_eq!(args.cache_control(), Some("no-cache"));
1596        assert_eq!(args.if_match(), Some("etag"));
1597        assert_eq!(args.if_none_match(), Some("other-etag"));
1598        assert_eq!(args.if_version_match(), Some("version-match"));
1599        assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1600        assert!(args.if_not_exists());
1601        assert_eq!(args.user_metadata().unwrap().get("owner"), Some("opendal"));
1602        assert!(args.is_conditional());
1603    }
1604
1605    #[test]
1606    fn compose_options_preserve_owned_views() {
1607        let args = OpCompose::from_options(
1608            &Capability::default(),
1609            options::ComposeOptions {
1610                concurrent: 4,
1611                content_type: Some("text/plain".to_owned()),
1612                content_disposition: Some("attachment".to_owned()),
1613                content_encoding: Some("gzip".to_owned()),
1614                cache_control: Some("no-cache".to_owned()),
1615                if_match: Some("etag".to_owned()),
1616                if_none_match: Some("other-etag".to_owned()),
1617                if_version_match: Some("version-match".to_owned()),
1618                if_version_not_match: Some("version-not-match".to_owned()),
1619                if_not_exists: true,
1620                user_metadata: Some(HashMap::from([("owner".to_owned(), "opendal".to_owned())])),
1621                ..Default::default()
1622            },
1623        )
1624        .unwrap();
1625
1626        assert_eq!(args.concurrent(), 4);
1627        assert_eq!(args.content_type(), Some("text/plain"));
1628        assert_eq!(args.content_disposition(), Some("attachment"));
1629        assert_eq!(args.content_encoding(), Some("gzip"));
1630        assert_eq!(args.cache_control(), Some("no-cache"));
1631        assert_eq!(args.if_match(), Some("etag"));
1632        assert_eq!(args.if_none_match(), Some("other-etag"));
1633        assert_eq!(args.if_version_match(), Some("version-match"));
1634        assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1635        assert!(args.if_not_exists());
1636        assert_eq!(args.user_metadata().unwrap().get("owner"), Some("opendal"));
1637        assert!(args.is_conditional());
1638    }
1639
1640    #[test]
1641    fn options_lower_if_not_changed_before_freeze() {
1642        let condition = condition_metadata();
1643        let capability = Capability {
1644            write_with_if_match: true,
1645            write_with_if_version_match: true,
1646            delete_with_if_match: true,
1647            copy_with_if_match: true,
1648            ..Default::default()
1649        };
1650        let (write, _) = OpWrite::from_options(
1651            &capability,
1652            options::WriteOptions {
1653                if_not_changed: Some(condition.clone()),
1654                ..Default::default()
1655            },
1656        )
1657        .unwrap();
1658        assert_eq!(write.if_match(), None);
1659        assert_eq!(write.if_version_match(), Some("version"));
1660
1661        let delete = OpDelete::from_options(
1662            &capability,
1663            options::DeleteOptions {
1664                if_not_changed: Some(condition.clone()),
1665                ..Default::default()
1666            },
1667        )
1668        .unwrap();
1669        assert_eq!(delete.if_match(), Some("etag"));
1670        assert_eq!(delete.if_version_match(), None);
1671
1672        let copy = OpCopy::from_options(
1673            &capability,
1674            options::CopyOptions {
1675                if_not_changed: Some(condition.clone()),
1676                ..Default::default()
1677            },
1678        )
1679        .unwrap();
1680        assert_eq!(copy.if_match(), Some("etag"));
1681        assert_eq!(copy.if_version_match(), None);
1682
1683        let compose = OpCompose::from_options(
1684            &capability,
1685            options::ComposeOptions {
1686                if_not_changed: Some(condition.clone()),
1687                ..Default::default()
1688            },
1689        )
1690        .unwrap();
1691        assert_eq!(compose.if_match(), Some("etag"));
1692        assert_eq!(compose.if_version_match(), None);
1693
1694        let compose_source = OpRead::from_compose_source_options(
1695            &capability,
1696            options::ComposeSourceOptions {
1697                if_not_changed: Some(condition.clone()),
1698                ..Default::default()
1699            },
1700        )
1701        .unwrap();
1702        assert_eq!(compose_source.if_match(), Some("etag"));
1703        assert_eq!(compose_source.version(), None);
1704
1705        let compose_source = OpRead::from_compose_source_options(
1706            &Capability {
1707                compose_with_source_version: true,
1708                ..Default::default()
1709            },
1710            options::ComposeSourceOptions {
1711                if_not_changed: Some(condition),
1712                ..Default::default()
1713            },
1714        )
1715        .unwrap();
1716        assert_eq!(compose_source.if_match(), None);
1717        assert_eq!(compose_source.version(), Some("version"));
1718    }
1719
1720    #[test]
1721    fn delete_copy_list_and_restore_options_roundtrip() {
1722        let delete = OpDelete::from_options(
1723            &Capability::default(),
1724            options::DeleteOptions {
1725                version: Some("version".to_owned()),
1726                recursive: true,
1727                if_match: Some("etag".to_owned()),
1728                ..Default::default()
1729            },
1730        )
1731        .unwrap();
1732        assert_eq!(delete.version(), Some("version"));
1733        assert!(delete.recursive());
1734        assert_eq!(delete.if_match(), Some("etag"));
1735        assert!(delete.is_conditional());
1736
1737        let copy = OpCopy::from_options(
1738            &Capability::default(),
1739            options::CopyOptions {
1740                if_not_exists: true,
1741                if_version_match: Some("destination-version".to_owned()),
1742                source_version: Some("source-version".to_owned()),
1743                ..Default::default()
1744            },
1745        )
1746        .unwrap();
1747        assert!(copy.if_not_exists());
1748        assert_eq!(copy.if_version_match(), Some("destination-version"));
1749        assert_eq!(copy.source_version(), Some("source-version"));
1750        assert!(copy.is_conditional());
1751
1752        let list: OpList = options::ListOptions {
1753            limit: Some(100),
1754            start_after: Some("marker".to_owned()),
1755            recursive: true,
1756            versions: true,
1757            deleted: true,
1758        }
1759        .into();
1760        assert_eq!(list.limit(), Some(100));
1761        assert_eq!(list.start_after(), Some("marker"));
1762        assert!(list.recursive());
1763        assert!(list.versions());
1764        assert!(list.deleted());
1765
1766        let restore: OpRestore = options::RestoreOptions {
1767            version: Some("version".to_owned()),
1768            if_not_exists: true,
1769        }
1770        .into();
1771        assert_eq!(restore.version(), Some("version"));
1772        assert!(restore.if_not_exists());
1773        assert!(restore.is_conditional());
1774    }
1775}