1use crate::data_stream::{
2 BoxFissionDataStream, DataStreamId, DataStreamRegistry, FissionDataStreamError,
3};
4use serde::{Deserialize, Serialize};
5use std::marker::PhantomData;
6
7#[derive(Clone, Debug)]
9pub struct CapabilityCtx {
10 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 pub fn register_data_stream(&self, stream: BoxFissionDataStream) -> DataStreamId {
27 self.data_streams.register(stream)
28 }
29
30 pub fn open_data_stream(
33 &self,
34 id: DataStreamId,
35 ) -> Result<BoxFissionDataStream, FissionDataStreamError> {
36 self.data_streams.open(id)
37 }
38
39 pub fn release_data_stream(&self, id: DataStreamId) -> bool {
41 self.data_streams.release(id)
42 }
43}
44
45pub 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#[derive(Copy, Clone)]
57pub struct CapabilityType<C: OperationCapability> {
58 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#[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#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
136pub struct PickedFile {
137 pub name: String,
139 pub content_type: Option<String>,
141 pub byte_len: Option<u64>,
143 pub stream: DataStreamId,
145}
146
147#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
149pub struct PickOpenFilesResult {
150 pub files: Vec<PickedFile>,
151}
152
153#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
155pub struct PickOpenFilesError {
156 pub code: String,
157 pub message: String,
158}
159
160impl PickOpenFilesError {
161 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 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}