diff --git a/core/core/src/docs/rfcs/7700_capability_api.md b/core/core/src/docs/rfcs/7700_capability_api.md new file mode 100644 index 000000000000..fa4ecf817340 --- /dev/null +++ b/core/core/src/docs/rfcs/7700_capability_api.md @@ -0,0 +1,265 @@ +- Proposal Name: `capability_api` +- Start Date: 2026-06-05 +- RFC PR: [apache/opendal#7700](https://github.com/apache/opendal/pull/7700) +- Tracking Issue: [apache/opendal#0000](https://github.com/apache/opendal/issues/0000) + +# Summary + +Rename OpenDAL's capability APIs around one user-facing concept: +`Capability` describes what the current `Operator` can do. Internally, +`AccessorInfo` will still keep both the service-declared capability and the +effective operator capability so layers can simulate, complete, or override +behavior without changing the service declaration. + +# Motivation + +OpenDAL currently exposes `native_capability()` and `full_capability()` on +`OperatorInfo`. The split is technically useful, but it makes the public API +harder to use correctly: + +- Users usually need to know whether an operation is available on the current + `Operator`. That is the effective capability. +- Layers such as `SimulateLayer`, `CompleteLayer`, and + `CapabilityOverrideLayer` need to know or update different capability states. +- The names `native` and `full` describe implementation history instead of the + contract that callers should rely on. + +The result is a confusing public model. A caller can read +`native_capability().delete_with_recursive == false` and conclude that +recursive delete is unavailable, even though `full_capability()` may be `true` +after simulation. OpenDAL should make the availability check obvious and keep +the service-declared state as an internal implementation boundary. + +# Guide-level explanation + +Users should use `OperatorInfo::capability()` to check whether a feature is +available: + +```rust +let cap = op.info().capability(); + +if cap.delete_with_recursive { + op.delete_with("path/").recursive(true).await?; +} +``` + +`Capability` remains the only public capability type. It describes the current +operator after services, layers, simulations, and overrides have been applied. + +Service authors and layer authors should use the service capability only when +they need the service-declared baseline. For example, `SimulateLayer` checks the +service capability to decide whether it should synthesize a missing operation, +then updates the effective capability to describe the new behavior. + +The old Rust methods remain as deprecated compatibility aliases during the +transition: + +```diff ++ pub fn capability(&self) -> Capability ++ pub(crate) or raw fn service_capability(&self) -> Capability +- pub fn full_capability(&self) -> Capability +- pub fn native_capability(&self) -> Capability +``` + +New user-facing examples and binding APIs should prefer `capability()`. +Bindings that already expose `capability()` keep their current behavior. +Bindings that expose `FullCapability` and `NativeCapability` should add +`Capability` as the primary property and deprecate the old names. + +# Reference-level explanation + +`Capability` keeps its existing fields and representation. This RFC does not +introduce a new `ServiceCapability` type. The service-declared and effective +states have the same shape today, and adding a new wrapper type would not add a +new contract. + +`AccessorInfoInner` will be renamed from: + +```rust +struct AccessorInfoInner { + native_capability: Capability, + full_capability: Capability, +} +``` + +to: + +```rust +struct AccessorInfoInner { + service_capability: Capability, + capability: Capability, +} +``` + +The fields have these meanings: + +- `service_capability`: the capability declared by the service implementation. +- `capability`: the effective capability of the accessor after layers have been + applied. + +`AccessorInfo` will provide the following methods: + +```rust +impl AccessorInfo { + pub fn capability(&self) -> Capability; + pub fn service_capability(&self) -> Capability; + + pub fn set_service_capability(&self, capability: Capability) -> &Self; + pub fn update_capability(&self, f: impl FnOnce(Capability) -> Capability) -> &Self; +} +``` + +`set_service_capability` also resets `capability` to the same value. This keeps +the current invariant of `set_native_capability`: services declare the baseline +once, and layers derive the effective capability from that baseline. + +The existing methods become deprecated aliases: + +```rust +impl AccessorInfo { + #[deprecated(note = "use service_capability()")] + pub fn native_capability(&self) -> Capability; + + #[deprecated(note = "use set_service_capability()")] + pub fn set_native_capability(&self, capability: Capability) -> &Self; + + #[deprecated(note = "use capability()")] + pub fn full_capability(&self) -> Capability; + + #[deprecated(note = "use update_capability()")] + pub fn update_full_capability(&self, f: impl FnOnce(Capability) -> Capability) -> &Self; +} +``` + +`OperatorInfo` will expose the effective capability: + +```rust +impl OperatorInfo { + pub fn capability(&self) -> Capability; + + #[deprecated(note = "use capability()")] + pub fn full_capability(&self) -> Capability; + + #[deprecated(note = "service capability is not intended for availability checks")] + pub fn native_capability(&self) -> Capability; +} +``` + +The stable user-facing API should not add `OperatorInfo::service_capability()` +in the first implementation. The service capability is an implementation detail +for service and layer authors. If a real application use case requires exposing +it later, it can be added as a separate API with clear documentation that it is +not an availability check. + +Layer behavior will be updated as follows: + +- `SimulateLayer` reads `service_capability()` and updates `capability()`. +- `CapabilityOverrideLayer` updates `capability()`. +- `CorrectnessCheckLayer` and `CapabilityCheckLayer` read `capability()`. +- Helper code that chooses part sizes, batch sizes, or writer behavior reads + `capability()`. + +Service implementations will call `set_service_capability()` instead of +`set_native_capability()`. + +# Compatibility and migration + +This change is source-compatible for Rust users during the transition because +the old APIs remain as deprecated aliases. + +Rust migration: + +```diff +- let cap = op.info().full_capability(); ++ let cap = op.info().capability(); +``` + +Service implementation migration: + +```diff +- info.set_native_capability(Capability { ... }); ++ info.set_service_capability(Capability { ... }); +``` + +Layer migration: + +```diff +- info.update_full_capability(|mut cap| { ++ info.update_capability(|mut cap| { + cap.delete_with_recursive = true; + cap + }); +``` + +Bindings should expose the effective capability as `capability` or +`Capability`. Existing `full_capability` and `native_capability` binding APIs +should be deprecated where the binding has a stable deprecation mechanism. + +No data migration is required. Capability values are runtime metadata and are +not persisted by OpenDAL. + +# Drawbacks + +This change adds another naming migration to an already large API surface. +OpenDAL services and layers contain many direct calls to the old methods, so the +implementation will touch many files even though the behavior stays the same. + +Keeping deprecated aliases also means the old names will continue to appear in +the codebase until the next breaking window. + +# Rationale and alternatives + +## Keep `native_capability` and `full_capability` + +This keeps the current implementation simple, but it preserves the main user +confusion. The public API continues to present two capability sets even though +only one of them answers whether a feature is available. + +## Add a new `ServiceCapability` type + +`ServiceCapability` would make the internal distinction stronger at the type +level. However, it would have the same fields as `Capability` today. If it hides +those fields, service and layer code becomes harder to write. If it dereferences +to `Capability`, the type boundary becomes weak. + +The proposed design keeps the single `Capability` data model and uses method +names to define the semantic boundary. A dedicated `ServiceCapability` type can +be introduced later if service-declared capabilities diverge from effective +capabilities. + +## Expose `service_capability()` to users + +Exposing service capability could help advanced users reason about performance +or implementation provenance. It also makes it easy to use the wrong API for +feature gating. This RFC keeps the stable public model focused on +`capability()`. Service capability remains available inside raw/service/layer +implementation code. + +# Prior art + +RFC-2852 introduced `native_capability` and `full_capability` to distinguish +service-native behavior from behavior implemented by layers. This RFC keeps the +same internal distinction but changes the public teaching model. + +`CapabilityOverrideLayer` already treats the effective capability as the value +that should be changed by endpoint-specific overrides. `SimulateLayer` already +uses the service-declared capability to decide whether simulation is needed and +then updates the effective capability. This RFC gives those existing roles +stable names. + +# Unresolved questions + +- Should `OperatorInfo::native_capability()` be deprecated immediately in all + bindings, or only in Rust first? +- Should raw `AccessorInfo::service_capability()` be public within `opendal-core` + or restricted to crate-private APIs where possible? + +# Future possibilities + +OpenDAL may introduce a dedicated `ServiceCapability` type if service-declared +capabilities need fields that are not meaningful for effective operator +capabilities. + +OpenDAL may also expose service capability through a diagnostic-only API if real +applications need to distinguish service-native behavior for cost or performance +planning. diff --git a/core/core/src/docs/rfcs/mod.rs b/core/core/src/docs/rfcs/mod.rs index 4d94e7733d56..546eadeeccad 100644 --- a/core/core/src/docs/rfcs/mod.rs +++ b/core/core/src/docs/rfcs/mod.rs @@ -21,6 +21,10 @@ #[doc = include_str!("0000_example.md")] pub mod rfc_0000_example {} +/// Capability API +#[doc = include_str!("7700_capability_api.md")] +pub mod rfc_7700_capability_api {} + /// Move Read Range To Reader #[doc = include_str!("7660_move_read_range_to_reader.md")] pub mod rfc_7660_move_read_range_to_reader {}