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
104    fn info(&self) -> ServiceInfo {
105        self.inner.info()
106    }
107
108    fn capability(&self) -> Capability {
109        self.inner.capability()
110    }
111
112    async fn create_dir(
113        &self,
114        ctx: &OperationContext,
115        path: &str,
116        args: OpCreateDir,
117    ) -> Result<RpCreateDir> {
118        self.inner.create_dir(ctx, path, args).await
119    }
120
121    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
122        self.inner.stat(ctx, path, args).await
123    }
124
125    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
126        self.inner.read(ctx, path, args)
127    }
128
129    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
130        let capability = self.capability();
131        let info = self.info();
132        if !capability.write_with_content_type && args.content_type().is_some() {
133            return Err(new_unsupported_error(
134                &info,
135                Operation::Write,
136                "content_type",
137            ));
138        }
139        if !capability.write_with_cache_control && args.cache_control().is_some() {
140            return Err(new_unsupported_error(
141                &info,
142                Operation::Write,
143                "cache_control",
144            ));
145        }
146        if !capability.write_with_content_disposition && args.content_disposition().is_some() {
147            return Err(new_unsupported_error(
148                &info,
149                Operation::Write,
150                "content_disposition",
151            ));
152        }
153
154        self.inner.write(ctx, path, args)
155    }
156
157    fn copy(
158        &self,
159        ctx: &OperationContext,
160        from: &str,
161        to: &str,
162        args: OpCopy,
163        opts: OpCopier,
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, opts)
186    }
187
188    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
189        self.inner.delete(ctx)
190    }
191
192    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
193        let capability = self.capability();
194        if !capability.list_with_versions && args.versions() {
195            let info = self.info();
196            return Err(new_unsupported_error(&info, Operation::List, "version"));
197        }
198
199        self.inner.list(ctx, path, args)
200    }
201
202    async fn rename(
203        &self,
204        ctx: &OperationContext,
205        from: &str,
206        to: &str,
207        args: OpRename,
208    ) -> Result<RpRename> {
209        self.inner.rename(ctx, from, to, args).await
210    }
211
212    async fn presign(
213        &self,
214        ctx: &OperationContext,
215        path: &str,
216        args: OpPresign,
217    ) -> Result<RpPresign> {
218        self.inner.presign(ctx, path, args).await
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[derive(Debug)]
227    struct MockService {
228        capability: Capability,
229    }
230
231    impl Service for MockService {
232        type Reader = ();
233        type Writer = ();
234        type Lister = ();
235        type Deleter = ();
236        type Copier = ();
237
238        fn info(&self) -> ServiceInfo {
239            ServiceInfo::with_scheme("mock")
240        }
241
242        fn capability(&self) -> Capability {
243            self.capability
244        }
245
246        async fn create_dir(
247            &self,
248            _: &OperationContext,
249            _: &str,
250            _: OpCreateDir,
251        ) -> Result<RpCreateDir> {
252            Err(Error::new(
253                ErrorKind::Unsupported,
254                "operation is not supported",
255            ))
256        }
257
258        async fn stat(&self, _: &OperationContext, _: &str, _: OpStat) -> Result<RpStat> {
259            Err(Error::new(
260                ErrorKind::Unsupported,
261                "operation is not supported",
262            ))
263        }
264
265        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
266            Err(Error::new(
267                ErrorKind::Unsupported,
268                "operation is not supported",
269            ))
270        }
271
272        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
273            Ok(())
274        }
275
276        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
277            Ok(())
278        }
279
280        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
281            Err(Error::new(
282                ErrorKind::Unsupported,
283                "operation is not supported",
284            ))
285        }
286
287        fn copy(
288            &self,
289            _: &OperationContext,
290            _: &str,
291            _: &str,
292            _: OpCopy,
293            _: OpCopier,
294        ) -> Result<Self::Copier> {
295            Err(Error::new(
296                ErrorKind::Unsupported,
297                "operation is not supported",
298            ))
299        }
300
301        async fn rename(
302            &self,
303            _: &OperationContext,
304            _: &str,
305            _: &str,
306            _: OpRename,
307        ) -> Result<RpRename> {
308            Err(Error::new(
309                ErrorKind::Unsupported,
310                "operation is not supported",
311            ))
312        }
313
314        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
315            Err(Error::new(
316                ErrorKind::Unsupported,
317                "operation is not supported",
318            ))
319        }
320    }
321
322    fn new_test_operator(capability: Capability) -> Operator {
323        let srv = MockService { capability };
324
325        Operator::from_parts(OperationContext::default(), Arc::new(srv))
326            .layer(CapabilityCheckLayer::new())
327    }
328
329    #[tokio::test]
330    async fn test_writer_with() {
331        let op = new_test_operator(Capability {
332            write: true,
333            ..Default::default()
334        });
335        let res = op.writer_with("path").content_type("type").await;
336        assert!(res.is_err());
337
338        let res = op.writer_with("path").cache_control("cache").await;
339        assert!(res.is_err());
340
341        let res = op
342            .writer_with("path")
343            .content_disposition("disposition")
344            .await;
345        assert!(res.is_err());
346
347        let op = new_test_operator(Capability {
348            write: true,
349            write_with_content_type: true,
350            write_with_cache_control: true,
351            write_with_content_disposition: true,
352            ..Default::default()
353        });
354        let res = op.writer_with("path").content_type("type").await;
355        assert!(res.is_ok());
356
357        let res = op.writer_with("path").cache_control("cache").await;
358        assert!(res.is_ok());
359
360        let res = op
361            .writer_with("path")
362            .content_disposition("disposition")
363            .await;
364        assert!(res.is_ok());
365    }
366
367    #[tokio::test]
368    async fn test_list_with() {
369        let op = new_test_operator(Capability {
370            list: true,
371            ..Default::default()
372        });
373        let res = op.list_with("path/").versions(true).await;
374        assert!(res.is_err());
375        assert_eq!(res.unwrap_err().kind(), ErrorKind::Unsupported);
376
377        let op = new_test_operator(Capability {
378            list: true,
379            list_with_versions: true,
380            ..Default::default()
381        });
382        let res = op.lister_with("path/").versions(true).await;
383        assert!(res.is_ok())
384    }
385}