Skip to main content

lxmf_sdk/
backend.rs

1use crate::app::{Envelope, EnvelopeResponse, OperationRegistry};
2use crate::capability::{NegotiationRequest, NegotiationResponse};
3use crate::domain::{
4    AttachmentDownloadChunk, AttachmentDownloadChunkRequest, AttachmentId, AttachmentListRequest,
5    AttachmentListResult, AttachmentMeta, AttachmentStoreRequest, AttachmentUploadChunkAck,
6    AttachmentUploadChunkRequest, AttachmentUploadCommitRequest, AttachmentUploadSession,
7    AttachmentUploadStartRequest, ContactListRequest, ContactListResult, ContactRecord,
8    ContactUpdateRequest, IdentityAnnounceRequest, IdentityAnnounceResult,
9    IdentityBootstrapRequest, IdentityBundle, IdentityCreateRequest, IdentityImportRequest,
10    IdentityRef, IdentityResolveRequest, MarkerCreateRequest, MarkerDeleteRequest,
11    MarkerListRequest, MarkerListResult, MarkerRecord, MarkerUpdatePositionRequest,
12    PaperDecodeResult, PaperMessageEnvelope, PeerConnectionRequest, PeerConnectionResult,
13    PresenceListRequest, PresenceListResult, RemoteCommandRequest, RemoteCommandResponse,
14    RemoteCommandSession, RemoteCommandSessionListRequest, RemoteCommandSessionListResult,
15    RouterStats, RouterStoragePolicy, RouterStoragePolicyPatch, TelemetryPoint, TelemetryQuery,
16    TopicCreateRequest, TopicId, TopicListRequest, TopicListResult, TopicPublishRequest,
17    TopicRecord, TopicSubscriptionRequest, VoiceSessionId, VoiceSessionOpenRequest,
18    VoiceSessionState, VoiceSessionUpdateRequest,
19};
20use crate::error::{code, ErrorCategory, SdkError};
21use crate::event::{EventBatch, EventCursor};
22#[cfg(feature = "sdk-async")]
23use crate::event::{EventSubscription, SdkEvent, SubscriptionStart};
24use crate::types::{
25    Ack, CancelResult, ConfigPatch, DeliverySnapshot, MessageId, RuntimeSnapshot, SendRequest,
26    ShutdownMode, TickBudget, TickResult,
27};
28use serde::{Deserialize, Serialize};
29#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
30use serde_json::Map as JsonMap;
31#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
32use serde_json::Value as JsonValue;
33#[cfg(feature = "sdk-async")]
34use std::future::Future;
35#[cfg(feature = "sdk-async")]
36use std::pin::Pin;
37#[cfg(feature = "sdk-async")]
38use tokio_stream::Stream;
39
40const CAP_KEY_MANAGEMENT: &str = "sdk.capability.key_management";
41#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
42const LXMF_RAW_FIELDS_KEY: &str = "_lxmf_fields_msgpack_b64";
43
44#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
45fn lxmf_wire_fields_from_payload(payload: JsonValue) -> JsonValue {
46    let JsonValue::Object(mut map) = payload else {
47        return JsonValue::Null;
48    };
49
50    if let Some(JsonValue::Object(fields)) = map.remove("fields") {
51        return non_empty_fields(fields);
52    }
53
54    let fields = map
55        .into_iter()
56        .filter(|(key, _)| !is_reserved_payload_field_key(key))
57        .collect::<JsonMap<String, JsonValue>>();
58    non_empty_fields(fields)
59}
60
61#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
62fn is_reserved_payload_field_key(key: &str) -> bool {
63    key != LXMF_RAW_FIELDS_KEY
64        && matches!(
65            key,
66            "title" | "content" | "body" | "payload" | "_lxmf" | "_sdk" | "_fields_raw"
67        )
68}
69
70#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
71fn non_empty_fields(fields: JsonMap<String, JsonValue>) -> JsonValue {
72    if fields.is_empty() {
73        JsonValue::Null
74    } else {
75        JsonValue::Object(fields)
76    }
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "snake_case")]
81pub enum KeyProviderClass {
82    InMemory,
83    File,
84    OsKeystore,
85    Hsm,
86    Custom(String),
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91pub enum SdkKeyPurpose {
92    IdentitySigning,
93    TransportDh,
94    SharedSecret,
95    Custom(String),
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
99pub struct SdkStoredKey {
100    pub key_id: String,
101    pub purpose: SdkKeyPurpose,
102    pub material: Vec<u8>,
103}
104
105pub trait SdkBackend: Send + Sync {
106    fn negotiate(&self, req: NegotiationRequest) -> Result<NegotiationResponse, SdkError>;
107
108    fn send(&self, req: SendRequest) -> Result<MessageId, SdkError>;
109
110    fn cancel(&self, id: MessageId) -> Result<CancelResult, SdkError>;
111
112    fn status(&self, id: MessageId) -> Result<Option<DeliverySnapshot>, SdkError>;
113
114    fn configure(&self, expected_revision: u64, patch: ConfigPatch) -> Result<Ack, SdkError>;
115
116    fn poll_events(&self, cursor: Option<EventCursor>, max: usize) -> Result<EventBatch, SdkError>;
117
118    fn snapshot(&self) -> Result<RuntimeSnapshot, SdkError>;
119
120    fn shutdown(&self, mode: ShutdownMode) -> Result<Ack, SdkError>;
121
122    fn router_stats(&self) -> Result<RouterStats, SdkError> {
123        Err(SdkError::capability_disabled("sdk.capability.router_management"))
124    }
125
126    fn router_storage_policy(&self) -> Result<RouterStoragePolicy, SdkError> {
127        Err(SdkError::capability_disabled("sdk.capability.router_management"))
128    }
129
130    fn set_router_storage_policy(
131        &self,
132        _patch: RouterStoragePolicyPatch,
133    ) -> Result<RouterStoragePolicy, SdkError> {
134        Err(SdkError::capability_disabled("sdk.capability.router_management"))
135    }
136
137    fn tick(&self, _budget: TickBudget) -> Result<TickResult, SdkError> {
138        Err(SdkError::new(
139            code::CAPABILITY_DISABLED,
140            ErrorCategory::Capability,
141            "backend does not support manual ticking",
142        ))
143    }
144
145    fn topic_create(&self, _req: TopicCreateRequest) -> Result<TopicRecord, SdkError> {
146        Err(SdkError::capability_disabled("sdk.capability.topics"))
147    }
148
149    fn topic_get(&self, _topic_id: TopicId) -> Result<Option<TopicRecord>, SdkError> {
150        Err(SdkError::capability_disabled("sdk.capability.topics"))
151    }
152
153    fn topic_list(&self, _req: TopicListRequest) -> Result<TopicListResult, SdkError> {
154        Err(SdkError::capability_disabled("sdk.capability.topics"))
155    }
156
157    fn topic_subscribe(&self, _req: TopicSubscriptionRequest) -> Result<Ack, SdkError> {
158        Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
159    }
160
161    fn topic_unsubscribe(&self, _topic_id: TopicId) -> Result<Ack, SdkError> {
162        Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
163    }
164
165    fn topic_publish(&self, _req: TopicPublishRequest) -> Result<Ack, SdkError> {
166        Err(SdkError::capability_disabled("sdk.capability.topic_fanout"))
167    }
168
169    fn telemetry_query(&self, _query: TelemetryQuery) -> Result<Vec<TelemetryPoint>, SdkError> {
170        Err(SdkError::capability_disabled("sdk.capability.telemetry_query"))
171    }
172
173    fn telemetry_subscribe(&self, _query: TelemetryQuery) -> Result<Ack, SdkError> {
174        Err(SdkError::capability_disabled("sdk.capability.telemetry_stream"))
175    }
176
177    fn attachment_store(&self, _req: AttachmentStoreRequest) -> Result<AttachmentMeta, SdkError> {
178        Err(SdkError::capability_disabled("sdk.capability.attachments"))
179    }
180
181    fn attachment_get(
182        &self,
183        _attachment_id: AttachmentId,
184    ) -> Result<Option<AttachmentMeta>, SdkError> {
185        Err(SdkError::capability_disabled("sdk.capability.attachments"))
186    }
187
188    fn attachment_list(
189        &self,
190        _req: AttachmentListRequest,
191    ) -> Result<AttachmentListResult, SdkError> {
192        Err(SdkError::capability_disabled("sdk.capability.attachments"))
193    }
194
195    fn attachment_delete(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
196        Err(SdkError::capability_disabled("sdk.capability.attachment_delete"))
197    }
198
199    fn attachment_download(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
200        Err(SdkError::capability_disabled("sdk.capability.attachments"))
201    }
202
203    fn attachment_upload_start(
204        &self,
205        _req: AttachmentUploadStartRequest,
206    ) -> Result<AttachmentUploadSession, SdkError> {
207        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
208    }
209
210    fn attachment_upload_chunk(
211        &self,
212        _req: AttachmentUploadChunkRequest,
213    ) -> Result<AttachmentUploadChunkAck, SdkError> {
214        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
215    }
216
217    fn attachment_upload_commit(
218        &self,
219        _req: AttachmentUploadCommitRequest,
220    ) -> Result<AttachmentMeta, SdkError> {
221        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
222    }
223
224    fn attachment_download_chunk(
225        &self,
226        _req: AttachmentDownloadChunkRequest,
227    ) -> Result<AttachmentDownloadChunk, SdkError> {
228        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
229    }
230
231    fn attachment_associate_topic(
232        &self,
233        _attachment_id: AttachmentId,
234        _topic_id: TopicId,
235    ) -> Result<Ack, SdkError> {
236        Err(SdkError::capability_disabled("sdk.capability.attachments"))
237    }
238
239    fn marker_create(&self, _req: MarkerCreateRequest) -> Result<MarkerRecord, SdkError> {
240        Err(SdkError::capability_disabled("sdk.capability.markers"))
241    }
242
243    fn marker_list(&self, _req: MarkerListRequest) -> Result<MarkerListResult, SdkError> {
244        Err(SdkError::capability_disabled("sdk.capability.markers"))
245    }
246
247    fn marker_update_position(
248        &self,
249        _req: MarkerUpdatePositionRequest,
250    ) -> Result<MarkerRecord, SdkError> {
251        Err(SdkError::capability_disabled("sdk.capability.markers"))
252    }
253
254    fn marker_delete(&self, _req: MarkerDeleteRequest) -> Result<Ack, SdkError> {
255        Err(SdkError::capability_disabled("sdk.capability.markers"))
256    }
257
258    fn identity_list(&self) -> Result<Vec<IdentityBundle>, SdkError> {
259        Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
260    }
261
262    fn identity_create(&self, _req: IdentityCreateRequest) -> Result<IdentityBundle, SdkError> {
263        Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
264    }
265
266    fn identity_announce_now(&self) -> Result<Ack, SdkError> {
267        Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
268    }
269
270    fn identity_announce(
271        &self,
272        _req: IdentityAnnounceRequest,
273    ) -> Result<IdentityAnnounceResult, SdkError> {
274        Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
275    }
276
277    fn identity_presence_list(
278        &self,
279        _req: PresenceListRequest,
280    ) -> Result<PresenceListResult, SdkError> {
281        Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
282    }
283
284    fn identity_activate(&self, _identity: IdentityRef) -> Result<Ack, SdkError> {
285        Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
286    }
287
288    fn identity_import(&self, _req: IdentityImportRequest) -> Result<IdentityBundle, SdkError> {
289        Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
290    }
291
292    fn identity_export(&self, _identity: IdentityRef) -> Result<IdentityImportRequest, SdkError> {
293        Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
294    }
295
296    fn identity_resolve(
297        &self,
298        _req: IdentityResolveRequest,
299    ) -> Result<Option<IdentityRef>, SdkError> {
300        Err(SdkError::capability_disabled("sdk.capability.identity_hash_resolution"))
301    }
302
303    fn identity_contact_update(
304        &self,
305        _req: ContactUpdateRequest,
306    ) -> Result<ContactRecord, SdkError> {
307        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
308    }
309
310    fn identity_contact_list(
311        &self,
312        _req: ContactListRequest,
313    ) -> Result<ContactListResult, SdkError> {
314        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
315    }
316
317    fn identity_bootstrap(
318        &self,
319        _req: IdentityBootstrapRequest,
320    ) -> Result<ContactRecord, SdkError> {
321        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
322    }
323
324    fn peer_connect(&self, _req: PeerConnectionRequest) -> Result<PeerConnectionResult, SdkError> {
325        Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
326    }
327
328    fn peer_disconnect(
329        &self,
330        _req: PeerConnectionRequest,
331    ) -> Result<PeerConnectionResult, SdkError> {
332        Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
333    }
334
335    fn peer_reconnect(
336        &self,
337        _req: PeerConnectionRequest,
338    ) -> Result<PeerConnectionResult, SdkError> {
339        Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
340    }
341
342    fn paper_encode(&self, _message_id: MessageId) -> Result<PaperMessageEnvelope, SdkError> {
343        Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
344    }
345
346    fn paper_decode(&self, _envelope: PaperMessageEnvelope) -> Result<Ack, SdkError> {
347        Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
348    }
349
350    fn paper_decode_with_metadata(
351        &self,
352        _envelope: PaperMessageEnvelope,
353    ) -> Result<PaperDecodeResult, SdkError> {
354        Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
355    }
356
357    fn command_invoke(
358        &self,
359        _req: RemoteCommandRequest,
360    ) -> Result<RemoteCommandResponse, SdkError> {
361        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
362    }
363
364    fn command_reply(
365        &self,
366        _correlation_id: String,
367        _reply: RemoteCommandResponse,
368    ) -> Result<Ack, SdkError> {
369        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
370    }
371
372    fn command_session_get(
373        &self,
374        _correlation_id: String,
375    ) -> Result<Option<RemoteCommandSession>, SdkError> {
376        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
377    }
378
379    fn command_session_list(
380        &self,
381        _req: RemoteCommandSessionListRequest,
382    ) -> Result<RemoteCommandSessionListResult, SdkError> {
383        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
384    }
385
386    fn voice_session_open(
387        &self,
388        _req: VoiceSessionOpenRequest,
389    ) -> Result<VoiceSessionId, SdkError> {
390        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
391    }
392
393    fn voice_session_update(
394        &self,
395        _req: VoiceSessionUpdateRequest,
396    ) -> Result<VoiceSessionState, SdkError> {
397        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
398    }
399
400    fn voice_session_close(&self, _session_id: VoiceSessionId) -> Result<Ack, SdkError> {
401        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
402    }
403
404    fn operation_registry(&self) -> Result<OperationRegistry, SdkError> {
405        Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
406    }
407
408    fn envelope_execute(&self, _envelope: Envelope) -> Result<EnvelopeResponse, SdkError> {
409        Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
410    }
411}
412
413pub trait SdkBackendKeyManagement: SdkBackend {
414    fn key_provider_class(&self) -> Result<KeyProviderClass, SdkError> {
415        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
416    }
417
418    fn key_get(&self, _key_id: &str) -> Result<Option<SdkStoredKey>, SdkError> {
419        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
420    }
421
422    fn key_put(&self, _key: SdkStoredKey) -> Result<Ack, SdkError> {
423        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
424    }
425
426    fn key_delete(&self, _key_id: &str) -> Result<Ack, SdkError> {
427        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
428    }
429
430    fn key_list_ids(&self) -> Result<Vec<String>, SdkError> {
431        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
432    }
433}
434
435#[cfg(feature = "sdk-async")]
436pub type SdkEventStream = Pin<Box<dyn Stream<Item = Result<SdkEvent, SdkError>> + Send>>;
437
438#[cfg(feature = "sdk-async")]
439pub type SdkBoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, SdkError>> + Send + 'a>>;
440
441#[cfg(feature = "sdk-async")]
442pub trait SdkBackendAsyncOps: SdkBackend {
443    fn negotiate_async(&self, req: NegotiationRequest) -> SdkBoxFuture<'_, NegotiationResponse>;
444
445    fn send_async(&self, req: SendRequest) -> SdkBoxFuture<'_, MessageId>;
446
447    fn cancel_async(&self, id: MessageId) -> SdkBoxFuture<'_, CancelResult> {
448        Box::pin(async move { self.cancel(id) })
449    }
450
451    fn status_async(&self, id: MessageId) -> SdkBoxFuture<'_, Option<DeliverySnapshot>>;
452
453    fn configure_async(&self, expected_revision: u64, patch: ConfigPatch) -> SdkBoxFuture<'_, Ack> {
454        Box::pin(async move { self.configure(expected_revision, patch) })
455    }
456
457    fn poll_events_async(
458        &self,
459        cursor: Option<EventCursor>,
460        max: usize,
461    ) -> SdkBoxFuture<'_, EventBatch> {
462        Box::pin(async move { self.poll_events(cursor, max) })
463    }
464
465    fn snapshot_async(&self) -> SdkBoxFuture<'_, RuntimeSnapshot>;
466
467    fn shutdown_async(&self, mode: ShutdownMode) -> SdkBoxFuture<'_, Ack>;
468}
469
470#[cfg(feature = "sdk-async")]
471pub trait SdkBackendAsyncEvents: SdkBackend {
472    fn subscribe_events(&self, start: SubscriptionStart) -> Result<EventSubscription, SdkError>;
473
474    fn open_event_stream(
475        &self,
476        _subscription: &EventSubscription,
477    ) -> Result<Option<SdkEventStream>, SdkError> {
478        Ok(None)
479    }
480}
481
482#[cfg(not(feature = "sdk-async"))]
483pub trait SdkBackendAsyncEvents: SdkBackend {}
484
485pub mod mobile_ble;
486
487#[cfg(all(feature = "rpc-backend", feature = "std"))]
488pub mod rpc;
489
490#[cfg(all(feature = "zmq-pipeline-backend", feature = "std"))]
491pub mod zmq_pipeline;
492
493#[cfg(test)]
494mod tests {
495    include!("backend_tests.rs");
496}