mockforge-plugin-cli 0.3.135

CLI tool for developing MockForge plugins
//! DataSource plugin template

pub const DATASOURCE_TEMPLATE: &str = r#"//! {{plugin_name}} - DataSource Plugin
//!
//! This plugin provides custom data source integration for MockForge.

use mockforge_plugin_sdk::prelude::*;

#[derive(Debug)]
pub struct Plugin {
    config: Option<serde_json::Value>,
    connection: Option<DataConnection>,
}

impl Default for Plugin {
    fn default() -> Self {
        Self::new()
    }
}

impl Plugin {
    pub fn new() -> Self {
        Self {
            config: None,
            connection: None,
        }
    }
}

#[async_trait]
impl DataSourcePlugin for Plugin {
    fn capabilities(&self) -> PluginCapabilities {
        PluginCapabilities {
            name: "{{plugin_name}}".to_string(),
            version: "0.1.0".to_string(),
            description: "Custom data source plugin".to_string(),
        }
    }

    async fn initialize(&mut self, config: serde_json::Value) -> PluginResult<()> {
        self.config = Some(config);
        Ok(())
    }

    async fn connect(&mut self, context: &PluginContext) -> PluginResult<DataConnection> {
        // TODO: Implement your connection logic here

        // Example: Create a mock connection
        let connection = DataConnection {
            id: format!("conn-{}", Uuid::new_v4()),
            source_type: "{{plugin_name}}".to_string(),
            connected: true,
            metadata: {
                let mut meta = HashMap::new();
                meta.insert("plugin_version".to_string(), json!("0.1.0"));
                meta.insert("connected_at".to_string(), json!(chrono::Utc::now().to_rfc3339()));
                meta
            },
        };

        self.connection = Some(connection.clone());
        Ok(connection)
    }

    async fn query(&self, context: &PluginContext, query: &DataQuery) -> PluginResult<DataResult> {
        // TODO: Implement your query logic here

        // Ensure we're connected
        if self.connection.is_none() {
            return Err(PluginError::InvalidState("Not connected".to_string()));
        }

        // Example: Handle different query types
        let rows = match query.query.as_str() {
            "SELECT * FROM users" | "users" => {
                vec![
                    DataRow {
                        values: {
                            let mut vals = HashMap::new();
                            vals.insert("id".to_string(), json!(1));
                            vals.insert("name".to_string(), json!("Alice"));
                            vals.insert("email".to_string(), json!("alice@example.com"));
                            vals
                        },
                    },
                    DataRow {
                        values: {
                            let mut vals = HashMap::new();
                            vals.insert("id".to_string(), json!(2));
                            vals.insert("name".to_string(), json!("Bob"));
                            vals.insert("email".to_string(), json!("bob@example.com"));
                            vals
                        },
                    },
                ]
            }
            "SELECT * FROM products" | "products" => {
                vec![
                    DataRow {
                        values: {
                            let mut vals = HashMap::new();
                            vals.insert("id".to_string(), json!(1));
                            vals.insert("name".to_string(), json!("Widget"));
                            vals.insert("price".to_string(), json!(9.99));
                            vals
                        },
                    },
                    DataRow {
                        values: {
                            let mut vals = HashMap::new();
                            vals.insert("id".to_string(), json!(2));
                            vals.insert("name".to_string(), json!("Gadget"));
                            vals.insert("price".to_string(), json!(19.99));
                            vals
                        },
                    },
                ]
            }
            _ => {
                vec![DataRow {
                    values: {
                        let mut vals = HashMap::new();
                        vals.insert("error".to_string(), json!("Unknown query"));
                        vals.insert("query".to_string(), json!(query.query.clone()));
                        vals
                    },
                }]
            }
        };

        // Apply limit from query
        let mut limited_rows = rows;
        if let Some(limit) = query.limit {
            limited_rows.truncate(limit);
        }

        Ok(DataResult {
            rows: limited_rows.clone(),
            total_count: limited_rows.len(),
            columns: self.get_columns_for_query(&query.query),
        })
    }

    async fn get_schema(&self, context: &PluginContext) -> PluginResult<Schema> {
        // TODO: Implement schema discovery

        let tables = vec![
            TableInfo {
                name: "users".to_string(),
                columns: vec![
                    ColumnInfo {
                        name: "id".to_string(),
                        data_type: "integer".to_string(),
                        nullable: false,
                    },
                    ColumnInfo {
                        name: "name".to_string(),
                        data_type: "string".to_string(),
                        nullable: false,
                    },
                    ColumnInfo {
                        name: "email".to_string(),
                        data_type: "string".to_string(),
                        nullable: true,
                    },
                ],
            },
            TableInfo {
                name: "products".to_string(),
                columns: vec![
                    ColumnInfo {
                        name: "id".to_string(),
                        data_type: "integer".to_string(),
                        nullable: false,
                    },
                    ColumnInfo {
                        name: "name".to_string(),
                        data_type: "string".to_string(),
                        nullable: false,
                    },
                    ColumnInfo {
                        name: "price".to_string(),
                        data_type: "number".to_string(),
                        nullable: false,
                    },
                ],
            },
        ];

        Ok(Schema { tables })
    }

    async fn test_connection(&self, context: &PluginContext) -> PluginResult<bool> {
        // TODO: Implement connection test
        // Example: Always return true for mock data source
        Ok(true)
    }

    async fn validate_config(&self, config: &serde_json::Value) -> PluginResult<()> {
        if !config.is_object() {
            return Err(PluginError::ConfigError(
                "Configuration must be an object".to_string()
            ));
        }
        Ok(())
    }

    async fn cleanup(&mut self) -> PluginResult<()> {
        self.connection = None;
        self.config = None;
        Ok(())
    }
}

impl Plugin {
    fn get_columns_for_query(&self, query: &str) -> Vec<ColumnInfo> {
        match query {
            q if q.contains("users") => vec![
                ColumnInfo {
                    name: "id".to_string(),
                    data_type: "integer".to_string(),
                    nullable: false,
                },
                ColumnInfo {
                    name: "name".to_string(),
                    data_type: "string".to_string(),
                    nullable: false,
                },
                ColumnInfo {
                    name: "email".to_string(),
                    data_type: "string".to_string(),
                    nullable: true,
                },
            ],
            q if q.contains("products") => vec![
                ColumnInfo {
                    name: "id".to_string(),
                    data_type: "integer".to_string(),
                    nullable: false,
                },
                ColumnInfo {
                    name: "name".to_string(),
                    data_type: "string".to_string(),
                    nullable: false,
                },
                ColumnInfo {
                    name: "price".to_string(),
                    data_type: "number".to_string(),
                    nullable: false,
                },
            ],
            _ => vec![],
        }
    }
}

// Export the plugin
export_plugin!(Plugin);

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_connect() {
        let mut plugin = Plugin::new();
        let mut harness = TestHarness::new();
        let context = harness.create_context("{{plugin_id}}", "test-request");

        let result = plugin.connect(&context).await;

        assert_plugin_ok!(result);
        if let Ok(connection) = result {
            assert!(connection.connected);
            assert_eq!(connection.source_type, "{{plugin_name}}");
        }
    }

    #[tokio::test]
    async fn test_query_users() {
        let mut plugin = Plugin::new();
        let mut harness = TestHarness::new();
        let context = harness.create_context("{{plugin_id}}", "test-request");

        // Connect first
        plugin.connect(&context).await.unwrap();

        let query = DataQuery {
            query: "SELECT * FROM users".to_string(),
            parameters: HashMap::new(),
            limit: None,
            offset: None,
        };

        let result = plugin.query(&context, &query).await;

        assert_plugin_ok!(result);
        if let Ok(data_result) = result {
            assert_eq!(data_result.rows.len(), 2);
            assert_eq!(data_result.total_count, 2);
            assert_eq!(data_result.columns.len(), 3);
        }
    }

    #[tokio::test]
    async fn test_query_with_limit() {
        let mut plugin = Plugin::new();
        let mut harness = TestHarness::new();
        let context = harness.create_context("{{plugin_id}}", "test-request");

        plugin.connect(&context).await.unwrap();

        let query = DataQuery {
            query: "SELECT * FROM users".to_string(),
            parameters: HashMap::new(),
            limit: Some(1),
            offset: None,
        };

        let result = plugin.query(&context, &query).await;

        assert_plugin_ok!(result);
        if let Ok(data_result) = result {
            assert_eq!(data_result.rows.len(), 1);
        }
    }

    #[tokio::test]
    async fn test_get_schema() {
        let plugin = Plugin::new();
        let mut harness = TestHarness::new();
        let context = harness.create_context("{{plugin_id}}", "test-request");

        let result = plugin.get_schema(&context).await;

        assert_plugin_ok!(result);
        if let Ok(schema) = result {
            assert_eq!(schema.tables.len(), 2);
            assert!(schema.tables.iter().any(|t| t.name == "users"));
            assert!(schema.tables.iter().any(|t| t.name == "products"));
        }
    }

    #[tokio::test]
    async fn test_connection_test() {
        let plugin = Plugin::new();
        let mut harness = TestHarness::new();
        let context = harness.create_context("{{plugin_id}}", "test-request");

        let result = plugin.test_connection(&context).await;

        assert_plugin_ok!(result);
        if let Ok(connected) = result {
            assert!(connected);
        }
    }
}
"#;