Skip to main content

fission_core/
capability.rs

1use crate::data_stream::{
2    BoxFissionDataStream, DataStreamId, DataStreamRegistry, FissionDataStreamError,
3};
4use serde::{Deserialize, Serialize};
5use std::marker::PhantomData;
6
7/// Context passed to an operation capability provider.
8#[derive(Clone, Debug)]
9pub struct CapabilityCtx {
10    /// Request id that identifies the corresponding effect envelope.
11    pub req_id: u64,
12    data_streams: DataStreamRegistry,
13}
14
15impl CapabilityCtx {
16    #[doc(hidden)]
17    pub fn new_runtime(req_id: u64, data_streams: DataStreamRegistry) -> Self {
18        Self {
19            req_id,
20            data_streams,
21        }
22    }
23
24    /// Registers a host-owned data stream and returns the handle that can be
25    /// passed back through a capability result.
26    pub fn register_data_stream(&self, stream: BoxFissionDataStream) -> DataStreamId {
27        self.data_streams.register(stream)
28    }
29
30    /// Opens a runtime-owned data stream for capability implementations that
31    /// need to consume stream handles supplied by app code.
32    pub fn open_data_stream(
33        &self,
34        id: DataStreamId,
35    ) -> Result<BoxFissionDataStream, FissionDataStreamError> {
36        self.data_streams.open(id)
37    }
38
39    /// Releases a previously registered stream without consuming it.
40    pub fn release_data_stream(&self, id: DataStreamId) -> bool {
41        self.data_streams.release(id)
42    }
43}
44
45/// Trait for one-shot host capabilities.
46///
47/// Capability payload types are fully typed and serialized by the host layer.
48/// Callers pass a `CapabilityType<C>` marker plus a typed `C::Request`.
49pub trait OperationCapability: Send + 'static {
50    type Request: Serialize + for<'de> Deserialize<'de> + Send + 'static;
51    type Ok: Serialize + for<'de> Deserialize<'de> + Send + 'static;
52    type Err: Serialize + for<'de> Deserialize<'de> + Send + 'static;
53}
54
55/// A typed capability identity.
56#[derive(Copy, Clone)]
57pub struct CapabilityType<C: OperationCapability> {
58    /// Capability name used by the shell registry and host providers.
59    pub name: &'static str,
60    _marker: PhantomData<fn() -> C>,
61}
62
63impl<C: OperationCapability> CapabilityType<C> {
64    pub const fn new(name: &'static str) -> Self {
65        Self {
66            name,
67            _marker: PhantomData,
68        }
69    }
70}
71
72impl<C: OperationCapability> std::fmt::Debug for CapabilityType<C> {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("CapabilityType")
75            .field("name", &self.name)
76            .finish()
77    }
78}
79
80impl<C: OperationCapability> PartialEq for CapabilityType<C> {
81    fn eq(&self, other: &Self) -> bool {
82        self.name == other.name
83    }
84}
85
86impl<C: OperationCapability> Eq for CapabilityType<C> {}
87
88impl<C: OperationCapability> std::hash::Hash for CapabilityType<C> {
89    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
90        self.name.hash(state);
91    }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub struct OperationCapabilityInvocation {
96    pub capability_name: String,
97    pub request: Vec<u8>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101pub enum CapabilityInvocationPayload {
102    Operation(OperationCapabilityInvocation),
103}
104
105#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
106pub struct OpenUrlRequest {
107    pub url: String,
108    pub in_app: bool,
109}
110
111pub struct OpenUrlCapability;
112
113impl OperationCapability for OpenUrlCapability {
114    type Request = OpenUrlRequest;
115    type Ok = ();
116    type Err = String;
117}
118
119pub const OPEN_URL: CapabilityType<OpenUrlCapability> = CapabilityType::new("fission.ui.open_url");
120
121/// Generic request for opening one or more local/user-granted files.
122///
123/// The contract is intentionally portable:
124/// - no raw local paths are exposed,
125/// - the shell chooses the native picker UI,
126/// - and selected files are returned as stream handles plus metadata.
127#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
128pub struct PickOpenFilesRequest {
129    pub allow_multiple: bool,
130    pub mime_types: Vec<String>,
131    pub extensions: Vec<String>,
132}
133
134/// A user-granted file returned from a picker capability.
135#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
136pub struct PickedFile {
137    /// User-facing file name supplied by the host.
138    pub name: String,
139    /// MIME type when the host can identify it.
140    pub content_type: Option<String>,
141    /// Total byte length when the host can determine it up front.
142    pub byte_len: Option<u64>,
143    /// Runtime-owned stream containing the selected file contents.
144    pub stream: DataStreamId,
145}
146
147/// Result payload for a file picker operation.
148#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
149pub struct PickOpenFilesResult {
150    pub files: Vec<PickedFile>,
151}
152
153/// Error returned by a file picker capability.
154#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
155pub struct PickOpenFilesError {
156    pub code: String,
157    pub message: String,
158}
159
160impl PickOpenFilesError {
161    /// Creates a portable file-picker error payload.
162    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
163        Self {
164            code: code.into(),
165            message: message.into(),
166        }
167    }
168
169    /// Creates a portable unsupported-operation error.
170    pub fn unsupported(operation: impl Into<String>) -> Self {
171        let operation = operation.into();
172        Self::new(
173            "unsupported",
174            format!("file picker operation `{operation}` is not supported by this host"),
175        )
176    }
177}
178
179pub struct PickOpenFilesCapability;
180
181impl OperationCapability for PickOpenFilesCapability {
182    type Request = PickOpenFilesRequest;
183    type Ok = PickOpenFilesResult;
184    type Err = PickOpenFilesError;
185}
186
187pub const PICK_OPEN_FILES: CapabilityType<PickOpenFilesCapability> =
188    CapabilityType::new("fission.fs.pick_open");
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn pick_open_files_round_trips() {
196        let request = PickOpenFilesRequest {
197            allow_multiple: true,
198            mime_types: vec!["image/png".into(), "application/pdf".into()],
199            extensions: vec!["png".into(), "pdf".into()],
200        };
201        let bytes = serde_json::to_vec(&request).unwrap();
202        let decoded: PickOpenFilesRequest = serde_json::from_slice(&bytes).unwrap();
203        assert_eq!(decoded, request);
204
205        let result = PickOpenFilesResult {
206            files: vec![PickedFile {
207                name: "receipt.pdf".into(),
208                content_type: Some("application/pdf".into()),
209                byte_len: Some(5),
210                stream: DataStreamId(7),
211            }],
212        };
213        let bytes = serde_json::to_vec(&result).unwrap();
214        let decoded: PickOpenFilesResult = serde_json::from_slice(&bytes).unwrap();
215        assert_eq!(decoded, result);
216    }
217
218    #[test]
219    fn open_url_round_trips() {
220        let request = OpenUrlRequest {
221            url: "https://fission.dev".into(),
222            in_app: false,
223        };
224
225        let bytes = serde_json::to_vec(&request).unwrap();
226        let decoded: OpenUrlRequest = serde_json::from_slice(&bytes).unwrap();
227
228        assert_eq!(decoded, request);
229    }
230}