Skip to main content

drasi_source_application/
lib.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(unexpected_cfgs)]
16
17//! Application Source Plugin for Drasi
18//!
19//! This plugin enables programmatic event injection into Drasi's continuous query
20//! processing pipeline. Unlike other sources that connect to external data systems,
21//! the application source allows your Rust code to directly send graph data changes.
22//!
23//! # Architecture
24//!
25//! The application source uses a handle-based pattern:
26//! - **`ApplicationSource`**: The source component that processes events
27//! - **`ApplicationSourceHandle`**: A cloneable handle for sending events from anywhere in your code
28//!
29//! # API Overview
30//!
31//! The `ApplicationSourceHandle` provides high-level methods for common operations:
32//!
33//! - [`send_node_insert`](ApplicationSourceHandle::send_node_insert) - Insert a new node
34//! - [`send_node_update`](ApplicationSourceHandle::send_node_update) - Update an existing node
35//! - [`send_delete`](ApplicationSourceHandle::send_delete) - Delete a node or relation
36//! - [`send_relation_insert`](ApplicationSourceHandle::send_relation_insert) - Insert a relationship
37//! - [`send_batch`](ApplicationSourceHandle::send_batch) - Send multiple changes efficiently
38//! - [`send`](ApplicationSourceHandle::send) - Send a raw `SourceChange` event
39//!
40//! # Building Properties
41//!
42//! Use the [`PropertyMapBuilder`] to construct property maps fluently:
43//!
44//! ```rust,ignore
45//! use drasi_source_application::PropertyMapBuilder;
46//!
47//! let props = PropertyMapBuilder::new()
48//!     .string("name", "Alice")
49//!     .integer("age", 30)
50//!     .float("score", 95.5)
51//!     .bool("active", true)
52//!     .build();
53//! ```
54//!
55//! # Configuration
56//!
57//! The application source has minimal configuration since it receives events programmatically
58//! rather than connecting to an external system.
59//!
60//! | Field | Type | Default | Description |
61//! |-------|------|---------|-------------|
62//! | `properties` | object | `{}` | Custom properties (passed through to `properties()`) |
63//!
64//! # Bootstrap Support
65//!
66//! The application source supports pluggable bootstrap providers via the `BootstrapProvider`
67//! trait. Configure a bootstrap provider using `set_bootstrap_provider()` or through the
68//! builder pattern. Common options include `ApplicationBootstrapProvider` for replaying
69//! stored events, or any other `BootstrapProvider` implementation.
70//!
71//! # Example Configuration (YAML)
72//!
73//! ```yaml
74//! source_type: application
75//! properties: {}
76//! ```
77//!
78//! # Usage Example
79//!
80//! ```rust,ignore
81//! use drasi_source_application::{
82//!     ApplicationSource, ApplicationSourceConfig, ApplicationSourceHandle,
83//!     PropertyMapBuilder
84//! };
85//! use std::sync::Arc;
86//!
87//! // Create the source and handle
88//! let config = ApplicationSourceConfig::default();
89//! let (source, handle) = ApplicationSource::new("my-app-source", config)?;
90//!
91//! // Add source to Drasi
92//! let source = Arc::new(source);
93//! drasi.add_source(source).await?;
94//!
95//! // Clone handle for use in different parts of your application
96//! let handle_clone = handle.clone();
97//!
98//! // Insert a node
99//! let props = PropertyMapBuilder::new()
100//!     .string("name", "Alice")
101//!     .integer("age", 30)
102//!     .build();
103//!
104//! handle.send_node_insert("user-1", vec!["User"], props).await?;
105//!
106//! // Insert a relationship
107//! let rel_props = PropertyMapBuilder::new()
108//!     .string("since", "2024-01-01")
109//!     .build();
110//!
111//! handle.send_relation_insert(
112//!     "follows-1",
113//!     vec!["FOLLOWS"],
114//!     rel_props,
115//!     "user-1",  // start node
116//!     "user-2",  // end node
117//! ).await?;
118//!
119//! // Update a node
120//! let updated_props = PropertyMapBuilder::new()
121//!     .integer("age", 31)
122//!     .build();
123//!
124//! handle.send_node_update("user-1", vec!["User"], updated_props).await?;
125//!
126//! // Delete a node
127//! handle.send_delete("user-1", vec!["User"]).await?;
128//! ```
129//!
130//! # Use Cases
131//!
132//! - **Testing**: Inject test data directly without setting up external sources
133//! - **Integration**: Bridge between your application logic and Drasi queries
134//! - **Simulation**: Generate synthetic events for development and demos
135//! - **Hybrid Sources**: Combine with other sources for complex data pipelines
136
137pub mod config;
138pub use config::ApplicationSourceConfig;
139
140mod property_builder;
141mod time;
142
143#[cfg(test)]
144mod tests;
145
146pub use property_builder::PropertyMapBuilder;
147
148use anyhow::Result;
149use async_trait::async_trait;
150use log::{debug, info, warn};
151use std::collections::HashMap;
152use std::sync::Arc;
153use tokio::sync::{mpsc, RwLock};
154
155use drasi_core::models::{Element, ElementMetadata, ElementReference, SourceChange};
156use drasi_lib::channels::{ComponentStatus, *};
157use drasi_lib::sources::base::{SourceBase, SourceBaseParams};
158use drasi_lib::Source;
159use tracing::Instrument;
160
161/// Handle for programmatic event injection into an Application Source
162///
163/// `ApplicationSourceHandle` provides a type-safe API for injecting graph data changes
164/// (node inserts, updates, deletes, and relationship inserts) directly from your application
165/// code into the Drasi continuous query processing pipeline.
166#[derive(Clone)]
167pub struct ApplicationSourceHandle {
168    tx: mpsc::Sender<SourceChange>,
169    source_id: String,
170}
171
172impl ApplicationSourceHandle {
173    /// Send a raw source change event
174    pub async fn send(&self, change: SourceChange) -> Result<()> {
175        self.tx
176            .send(change)
177            .await
178            .map_err(|_| anyhow::anyhow!("Failed to send event: channel closed"))?;
179        Ok(())
180    }
181
182    /// Insert a new node into the graph
183    pub async fn send_node_insert(
184        &self,
185        element_id: impl Into<Arc<str>>,
186        labels: Vec<impl Into<Arc<str>>>,
187        properties: drasi_core::models::ElementPropertyMap,
188    ) -> Result<()> {
189        let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
190            warn!("Failed to get timestamp for node insert: {e}, using fallback");
191            chrono::Utc::now().timestamp_millis() as u64
192        });
193
194        let element = Element::Node {
195            metadata: ElementMetadata {
196                reference: ElementReference {
197                    source_id: Arc::from(self.source_id.as_str()),
198                    element_id: element_id.into(),
199                },
200                labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
201                effective_from,
202            },
203            properties,
204        };
205
206        self.send(SourceChange::Insert { element }).await
207    }
208
209    /// Update an existing node in the graph
210    pub async fn send_node_update(
211        &self,
212        element_id: impl Into<Arc<str>>,
213        labels: Vec<impl Into<Arc<str>>>,
214        properties: drasi_core::models::ElementPropertyMap,
215    ) -> Result<()> {
216        let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
217            warn!("Failed to get timestamp for node update: {e}, using fallback");
218            chrono::Utc::now().timestamp_millis() as u64
219        });
220
221        let element = Element::Node {
222            metadata: ElementMetadata {
223                reference: ElementReference {
224                    source_id: Arc::from(self.source_id.as_str()),
225                    element_id: element_id.into(),
226                },
227                labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
228                effective_from,
229            },
230            properties,
231        };
232
233        self.send(SourceChange::Update { element }).await
234    }
235
236    /// Delete a node or relationship from the graph
237    pub async fn send_delete(
238        &self,
239        element_id: impl Into<Arc<str>>,
240        labels: Vec<impl Into<Arc<str>>>,
241    ) -> Result<()> {
242        let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
243            warn!("Failed to get timestamp for delete: {e}, using fallback");
244            chrono::Utc::now().timestamp_millis() as u64
245        });
246
247        let metadata = ElementMetadata {
248            reference: ElementReference {
249                source_id: Arc::from(self.source_id.as_str()),
250                element_id: element_id.into(),
251            },
252            labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
253            effective_from,
254        };
255
256        self.send(SourceChange::Delete { metadata }).await
257    }
258
259    /// Insert a new relationship into the graph
260    pub async fn send_relation_insert(
261        &self,
262        element_id: impl Into<Arc<str>>,
263        labels: Vec<impl Into<Arc<str>>>,
264        properties: drasi_core::models::ElementPropertyMap,
265        start_node_id: impl Into<Arc<str>>,
266        end_node_id: impl Into<Arc<str>>,
267    ) -> Result<()> {
268        let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
269            warn!("Failed to get timestamp for relation insert: {e}, using fallback");
270            chrono::Utc::now().timestamp_millis() as u64
271        });
272
273        let element = Element::Relation {
274            metadata: ElementMetadata {
275                reference: ElementReference {
276                    source_id: Arc::from(self.source_id.as_str()),
277                    element_id: element_id.into(),
278                },
279                labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
280                effective_from,
281            },
282            properties,
283            in_node: ElementReference {
284                source_id: Arc::from(self.source_id.as_str()),
285                element_id: start_node_id.into(),
286            },
287            out_node: ElementReference {
288                source_id: Arc::from(self.source_id.as_str()),
289                element_id: end_node_id.into(),
290            },
291        };
292
293        self.send(SourceChange::Insert { element }).await
294    }
295
296    /// Send a batch of source changes efficiently
297    pub async fn send_batch(&self, changes: Vec<SourceChange>) -> Result<()> {
298        for change in changes {
299            self.send(change).await?;
300        }
301        Ok(())
302    }
303
304    /// Get the source ID that this handle is connected to
305    pub fn source_id(&self) -> &str {
306        &self.source_id
307    }
308}
309
310/// A source that allows applications to programmatically inject events.
311///
312/// This source receives events from an [`ApplicationSourceHandle`] and forwards
313/// them to the Drasi query processing pipeline.
314///
315/// # Fields
316///
317/// - `base`: Common source functionality (dispatchers, status, lifecycle, bootstrap)
318/// - `config`: Application source configuration
319/// - `app_rx`: Receiver for events from the handle
320/// - `app_tx`: Sender for creating additional handles
321pub struct ApplicationSource {
322    /// Base source implementation providing common functionality
323    base: SourceBase,
324    /// Application source configuration
325    config: ApplicationSourceConfig,
326    /// Receiver for events from handles (taken when processing starts)
327    app_rx: Arc<RwLock<Option<mpsc::Receiver<SourceChange>>>>,
328    /// Sender for creating new handles
329    app_tx: mpsc::Sender<SourceChange>,
330}
331
332impl ApplicationSource {
333    /// Create a new application source and its handle.
334    ///
335    /// The event channel is automatically injected when the source is added
336    /// to DrasiLib via `add_source()`.
337    ///
338    /// # Arguments
339    ///
340    /// * `id` - Unique identifier for this source instance
341    /// * `config` - Application source configuration
342    ///
343    /// # Returns
344    ///
345    /// A tuple of `(ApplicationSource, ApplicationSourceHandle)` where the handle
346    /// can be used to send events to the source.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if the base source cannot be initialized.
351    ///
352    /// # Example
353    ///
354    /// ```rust,ignore
355    /// use drasi_source_application::{ApplicationSource, ApplicationSourceConfig};
356    ///
357    /// let config = ApplicationSourceConfig::default();
358    /// let (source, handle) = ApplicationSource::new("my-source", config)?;
359    /// ```
360    pub fn new(
361        id: impl Into<String>,
362        config: ApplicationSourceConfig,
363    ) -> Result<(Self, ApplicationSourceHandle)> {
364        let id = id.into();
365        let params = SourceBaseParams::new(id.clone());
366        let (app_tx, app_rx) = mpsc::channel(1000);
367
368        let handle = ApplicationSourceHandle {
369            tx: app_tx.clone(),
370            source_id: id.clone(),
371        };
372
373        let source = Self {
374            base: SourceBase::new(params)?,
375            config,
376            app_rx: Arc::new(RwLock::new(Some(app_rx))),
377            app_tx,
378        };
379
380        Ok((source, handle))
381    }
382
383    /// Get a new handle for this source
384    pub fn get_handle(&self) -> ApplicationSourceHandle {
385        ApplicationSourceHandle {
386            tx: self.app_tx.clone(),
387            source_id: self.base.id.clone(),
388        }
389    }
390
391    async fn process_events(&self) -> Result<()> {
392        let mut rx = self
393            .app_rx
394            .write()
395            .await
396            .take()
397            .ok_or_else(|| anyhow::anyhow!("Receiver already taken"))?;
398
399        let source_name = self.base.id.clone();
400        let base_dispatchers = self.base.dispatchers.clone();
401        let reporter = self.base.status_handle();
402        let source_id = self.base.id.clone();
403
404        // Get instance_id from context for log routing isolation
405        let instance_id = self
406            .base
407            .context()
408            .await
409            .map(|c| c.instance_id)
410            .unwrap_or_default();
411
412        let span = tracing::info_span!(
413            "application_source_processor",
414            instance_id = %instance_id,
415            component_id = %source_id,
416            component_type = "source"
417        );
418        let handle = tokio::spawn(
419            async move {
420                info!("ApplicationSource '{source_name}' event processor started");
421
422                reporter
423                    .set_status(
424                        ComponentStatus::Running,
425                        Some("Processing events".to_string()),
426                    )
427                    .await;
428
429                while let Some(change) = rx.recv().await {
430                    debug!("ApplicationSource '{source_name}' received event: {change:?}");
431
432                    let mut profiling = drasi_lib::profiling::ProfilingMetadata::new();
433                    profiling.source_send_ns = Some(drasi_lib::profiling::timestamp_ns());
434
435                    let wrapper = SourceEventWrapper::with_profiling(
436                        source_name.clone(),
437                        SourceEvent::Change(change),
438                        chrono::Utc::now(),
439                        profiling,
440                    );
441
442                    if let Err(e) = SourceBase::dispatch_from_task(
443                        base_dispatchers.clone(),
444                        wrapper,
445                        &source_name,
446                    )
447                    .await
448                    {
449                        debug!("Failed to dispatch change (no subscribers): {e}");
450                    }
451                }
452
453                info!("ApplicationSource '{source_name}' event processor stopped");
454            }
455            .instrument(span),
456        );
457
458        *self.base.task_handle.write().await = Some(handle);
459        Ok(())
460    }
461}
462
463#[async_trait]
464impl Source for ApplicationSource {
465    fn id(&self) -> &str {
466        &self.base.id
467    }
468
469    fn type_name(&self) -> &str {
470        "application"
471    }
472
473    fn properties(&self) -> HashMap<String, serde_json::Value> {
474        self.config.properties.clone()
475    }
476
477    fn auto_start(&self) -> bool {
478        self.base.get_auto_start()
479    }
480
481    async fn start(&self) -> Result<()> {
482        info!("Starting ApplicationSource '{}'", self.base.id);
483
484        self.base
485            .set_status(
486                ComponentStatus::Starting,
487                Some("Starting application source".to_string()),
488            )
489            .await;
490
491        self.process_events().await?;
492
493        Ok(())
494    }
495
496    async fn stop(&self) -> Result<()> {
497        info!("Stopping ApplicationSource '{}'", self.base.id);
498
499        self.base
500            .set_status(
501                ComponentStatus::Stopping,
502                Some("Stopping application source".to_string()),
503            )
504            .await;
505
506        if let Some(handle) = self.base.task_handle.write().await.take() {
507            handle.abort();
508        }
509
510        self.base
511            .set_status(
512                ComponentStatus::Stopped,
513                Some("Application source stopped".to_string()),
514            )
515            .await;
516
517        Ok(())
518    }
519
520    async fn status(&self) -> ComponentStatus {
521        self.base.get_status().await
522    }
523
524    async fn subscribe(
525        &self,
526        settings: drasi_lib::config::SourceSubscriptionSettings,
527    ) -> Result<SubscriptionResponse> {
528        self.base
529            .subscribe_with_bootstrap(&settings, "Application")
530            .await
531    }
532
533    fn as_any(&self) -> &dyn std::any::Any {
534        self
535    }
536
537    async fn initialize(&self, context: drasi_lib::context::SourceRuntimeContext) {
538        self.base.initialize(context).await;
539    }
540
541    async fn set_bootstrap_provider(
542        &self,
543        provider: Box<dyn drasi_lib::bootstrap::BootstrapProvider + 'static>,
544    ) {
545        self.base.set_bootstrap_provider(provider).await;
546    }
547}