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