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}