Skip to main content

opendal_layer_capability_check/
lib.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#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22use std::sync::Arc;
23
24use opendal_core::raw::*;
25use opendal_core::*;
26
27/// `CapabilityCheckLayer` validates optional operation arguments against service capabilities.
28///
29/// Similar to `CorrectnessChecker`, this layer verifies selected optional arguments for write,
30/// copy, and list operations against the capabilities of the underlying service. If an argument is
31/// not supported, an error is returned directly.
32///
33/// # Notes
34///
35/// There are two main differences between this checker with the `CorrectnessChecker`:
36/// 1. This checker provides additional checks for capabilities like write_with_content_type and
37///    list_with_versions, among others. These capabilities do not affect data integrity, even if
38///    the underlying storage services do not support them.
39///
40/// 2. OpenDAL doesn't apply this checker by default. Users can enable this layer if they want to
41///    enforce stricter requirements.
42///
43/// # Examples
44///
45/// ```no_run
46/// # use opendal_core::services;
47/// # use opendal_core::Operator;
48/// # use opendal_core::Result;
49/// # use opendal_layer_capability_check::CapabilityCheckLayer;
50/// #
51/// # fn main() -> Result<()> {
52/// let _ = Operator::new(services::Memory::default())?
53///     .layer(CapabilityCheckLayer::new());
54/// # Ok(())
55/// # }
56/// ```
57#[derive(Clone, Debug, Default)]
58#[non_exhaustive]
59pub struct CapabilityCheckLayer {}
60
61impl CapabilityCheckLayer {
62    /// Create a new [`CapabilityCheckLayer`].
63    pub fn new() -> Self {
64        Self::default()
65    }
66}
67
68impl Layer for CapabilityCheckLayer {
69    fn apply_service(&self, inner: Servicer) -> Servicer {
70        Arc::new(self.layer(inner))
71    }
72}
73
74impl CapabilityCheckLayer {
75    fn layer(&self, inner: Servicer) -> CapabilityCheckService {
76        CapabilityCheckService { inner }
77    }
78}
79
80#[doc(hidden)]
81#[derive(Debug)]
82pub struct CapabilityCheckService {
83    inner: Servicer,
84}
85
86fn new_unsupported_error(info: &ServiceInfo, op: Operation, args: &str) -> Error {
87    let scheme = info.scheme();
88    let op = op.into_static();
89
90    Error::new(
91        ErrorKind::Unsupported,
92        format!("The service {scheme} does not support the operation {op} with the arguments {args}. Please verify if the relevant flags have been enabled, or submit an issue if you believe this is incorrect."),
93    )
94    .with_operation(op)
95}
96
97impl Service for CapabilityCheckService {
98    type Reader = oio::Reader;
99    type Writer = oio::Writer;
100    type Lister = oio::Lister;
101    type Deleter = oio::Deleter;
102    type Copier = oio::Copier;
103    type Composer = oio::Composer;
104
105    fn info(&self) -> ServiceInfo {
106        self.inner.info()
107    }
108
109    fn capability(&self) -> Capability {
110        self.inner.capability()
111    }
112
113    async fn create_dir(
114        &self,
115        ctx: &OperationContext,
116        path: &str,
117        args: OpCreateDir,
118    ) -> Result<RpCreateDir> {
119        self.inner.create_dir(ctx, path, args).await
120    }
121
122    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
123        self.inner.stat(ctx, path, args).await
124    }
125
126    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
127        self.inner.read(ctx, path, args)
128    }
129
130    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
131        let capability = self.capability();
132        let info = self.info();
133        if !capability.write_with_content_type && args.content_type().is_some() {
134            return Err(new_unsupported_error(
135                &info,
136                Operation::Write,
137                "content_type",
138            ));
139        }
140        if !capability.write_with_cache_control && args.cache_control().is_some() {
141            return Err(new_unsupported_error(
142                &info,
143                Operation::Write,
144                "cache_control",
145            ));
146        }
147        if !capability.write_with_content_disposition && args.content_disposition().is_some() {
148            return Err(new_unsupported_error(
149                &info,
150                Operation::Write,
151                "content_disposition",
152            ));
153        }
154
155        self.inner.write(ctx, path, args)
156    }
157
158    fn copy(
159        &self,
160        ctx: &OperationContext,
161        from: &str,
162        to: &str,
163        args: OpCopy,
164    ) -> Result<Self::Copier> {
165        let capability = self.capability();
166        let info = self.info();
167        if args.if_not_exists() && !capability.copy_with_if_not_exists {
168            return Err(new_unsupported_error(
169                &info,
170                Operation::Copy,
171                "if_not_exists",
172            ));
173        }
174        if args.if_match().is_some() && !capability.copy_with_if_match {
175            return Err(new_unsupported_error(&info, Operation::Copy, "if_match"));
176        }
177        if args.source_version().is_some() && !capability.copy_with_source_version {
178            return Err(new_unsupported_error(
179                &info,
180                Operation::Copy,
181                "source_version",
182            ));
183        }
184
185        self.inner.copy(ctx, from, to, args)
186    }
187
188    fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
189        let capability = self.capability();
190        let info = self.info();
191        let checks = [
192            (
193                args.content_type().is_some(),
194                capability.compose_with_content_type,
195                "content_type",
196            ),
197            (
198                args.content_disposition().is_some(),
199                capability.compose_with_content_disposition,
200                "content_disposition",
201            ),
202            (
203                args.content_encoding().is_some(),
204                capability.compose_with_content_encoding,
205                "content_encoding",
206            ),
207            (
208                args.cache_control().is_some(),
209                capability.compose_with_cache_control,
210                "cache_control",
211            ),
212            (
213                args.user_metadata().is_some(),
214                capability.compose_with_user_metadata,
215                "user_metadata",
216            ),
217            (
218                args.if_match().is_some(),
219                capability.compose_with_if_match,
220                "if_match",
221            ),
222            (
223                args.if_none_match().is_some(),
224                capability.compose_with_if_none_match,
225                "if_none_match",
226            ),
227            (
228                args.if_version_match().is_some(),
229                capability.compose_with_if_version_match,
230                "if_version_match",
231            ),
232            (
233                args.if_version_not_match().is_some(),
234                capability.compose_with_if_version_not_match,
235                "if_version_not_match",
236            ),
237            (
238                args.if_not_exists(),
239                capability.compose_with_if_not_exists,
240                "if_not_exists",
241            ),
242        ];
243
244        if !capability.compose {
245            return Err(new_unsupported_error(&info, Operation::Compose, ""));
246        }
247        if let Some((_, _, name)) = checks
248            .iter()
249            .find(|(used, supported, _)| *used && !supported)
250        {
251            return Err(new_unsupported_error(&info, Operation::Compose, name));
252        }
253
254        self.inner.compose(ctx, to, args)
255    }
256
257    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
258        self.inner.delete(ctx)
259    }
260
261    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
262        let capability = self.capability();
263        if !capability.list_with_versions && args.versions() {
264            let info = self.info();
265            return Err(new_unsupported_error(&info, Operation::List, "version"));
266        }
267
268        self.inner.list(ctx, path, args)
269    }
270
271    async fn rename(
272        &self,
273        ctx: &OperationContext,
274        from: &str,
275        to: &str,
276        args: OpRename,
277    ) -> Result<RpRename> {
278        self.inner.rename(ctx, from, to, args).await
279    }
280
281    async fn restore(
282        &self,
283        ctx: &OperationContext,
284        path: &str,
285        args: OpRestore,
286    ) -> Result<RpRestore> {
287        let capability = self.capability();
288        let info = self.info();
289        if !capability.restore {
290            return Err(new_unsupported_error(&info, Operation::Restore, ""));
291        }
292        if args.version().is_some() && !capability.restore_with_version {
293            return Err(new_unsupported_error(&info, Operation::Restore, "version"));
294        }
295        if args.if_not_exists() && !capability.restore_with_if_not_exists {
296            return Err(new_unsupported_error(
297                &info,
298                Operation::Restore,
299                "if_not_exists",
300            ));
301        }
302
303        self.inner.restore(ctx, path, args).await
304    }
305
306    async fn presign(
307        &self,
308        ctx: &OperationContext,
309        path: &str,
310        args: OpPresign,
311    ) -> Result<RpPresign> {
312        self.inner.presign(ctx, path, args).await
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[derive(Debug)]
321    struct MockService {
322        capability: Capability,
323    }
324
325    impl Service for MockService {
326        type Reader = ();
327        type Writer = ();
328        type Lister = ();
329        type Deleter = ();
330        type Copier = ();
331        type Composer = ();
332
333        fn info(&self) -> ServiceInfo {
334            ServiceInfo::with_scheme("mock")
335        }
336
337        fn capability(&self) -> Capability {
338            self.capability
339        }
340
341        async fn create_dir(
342            &self,
343            _: &OperationContext,
344            _: &str,
345            _: OpCreateDir,
346        ) -> Result<RpCreateDir> {
347            Err(Error::new(
348                ErrorKind::Unsupported,
349                "operation is not supported",
350            ))
351        }
352
353        async fn stat(&self, _: &OperationContext, _: &str, _: OpStat) -> Result<RpStat> {
354            Err(Error::new(
355                ErrorKind::Unsupported,
356                "operation is not supported",
357            ))
358        }
359
360        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
361            Err(Error::new(
362                ErrorKind::Unsupported,
363                "operation is not supported",
364            ))
365        }
366
367        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
368            Ok(())
369        }
370
371        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
372            Ok(())
373        }
374
375        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
376            Err(Error::new(
377                ErrorKind::Unsupported,
378                "operation is not supported",
379            ))
380        }
381
382        fn copy(&self, _: &OperationContext, _: &str, _: &str, _: OpCopy) -> Result<Self::Copier> {
383            Err(Error::new(
384                ErrorKind::Unsupported,
385                "operation is not supported",
386            ))
387        }
388
389        async fn rename(
390            &self,
391            _: &OperationContext,
392            _: &str,
393            _: &str,
394            _: OpRename,
395        ) -> Result<RpRename> {
396            Err(Error::new(
397                ErrorKind::Unsupported,
398                "operation is not supported",
399            ))
400        }
401
402        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
403            Err(Error::new(
404                ErrorKind::Unsupported,
405                "operation is not supported",
406            ))
407        }
408    }
409
410    fn new_test_operator(capability: Capability) -> Operator {
411        let srv = MockService { capability };
412
413        Operator::from_parts(OperationContext::default(), Arc::new(srv))
414            .layer(CapabilityCheckLayer::new())
415    }
416
417    #[tokio::test]
418    async fn test_writer_with() {
419        let op = new_test_operator(Capability {
420            write: true,
421            ..Default::default()
422        });
423        let res = op.writer_with("path").content_type("type").await;
424        assert!(res.is_err());
425
426        let res = op.writer_with("path").cache_control("cache").await;
427        assert!(res.is_err());
428
429        let res = op
430            .writer_with("path")
431            .content_disposition("disposition")
432            .await;
433        assert!(res.is_err());
434
435        let op = new_test_operator(Capability {
436            write: true,
437            write_with_content_type: true,
438            write_with_cache_control: true,
439            write_with_content_disposition: true,
440            ..Default::default()
441        });
442        let res = op.writer_with("path").content_type("type").await;
443        assert!(res.is_ok());
444
445        let res = op.writer_with("path").cache_control("cache").await;
446        assert!(res.is_ok());
447
448        let res = op
449            .writer_with("path")
450            .content_disposition("disposition")
451            .await;
452        assert!(res.is_ok());
453    }
454
455    #[tokio::test]
456    async fn test_list_with() {
457        let op = new_test_operator(Capability {
458            list: true,
459            ..Default::default()
460        });
461        let res = op.list_with("path/").versions(true).await;
462        assert!(res.is_err());
463        assert_eq!(res.unwrap_err().kind(), ErrorKind::Unsupported);
464
465        let op = new_test_operator(Capability {
466            list: true,
467            list_with_versions: true,
468            ..Default::default()
469        });
470        let res = op.lister_with("path/").versions(true).await;
471        assert!(res.is_ok())
472    }
473}