use std::future::Future;
use std::pin::Pin;
use serde_json::Value;
use crate::error::EngineError;
pub trait Operation: Send + Sync {
fn kind(&self) -> &str;
fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>>;
fn input(&self) -> Option<Value> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use ironflow_core::error::OperationError;
use serde_json::json;
struct GitLabIssueOp {
project_id: u64,
title: String,
}
impl Operation for GitLabIssueOp {
fn kind(&self) -> &str {
"gitlab"
}
fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
Box::pin(async move {
Ok(json!({
"issue_id": 42,
"url": "https://gitlab.com/issues/42",
"project_id": self.project_id,
"title": self.title
}))
})
}
fn input(&self) -> Option<Value> {
Some(json!({
"project_id": self.project_id,
"title": self.title
}))
}
}
struct NoInputOp;
impl Operation for NoInputOp {
fn kind(&self) -> &str {
"noop"
}
fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
Box::pin(async { Ok(json!({"status": "ok"})) })
}
}
struct ErrorOp;
impl Operation for ErrorOp {
fn kind(&self) -> &str {
"error_test"
}
fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
Box::pin(async {
Err(EngineError::Operation(OperationError::Http {
status: Some(500),
message: "test error".to_string(),
}))
})
}
}
#[test]
fn operation_kind_identifies_operation_type() {
let op = GitLabIssueOp {
project_id: 123,
title: "Bug".to_string(),
};
assert_eq!(op.kind(), "gitlab");
}
#[test]
fn operation_with_input_provides_structured_logging() {
let op = GitLabIssueOp {
project_id: 456,
title: "Feature request".to_string(),
};
let input = op.input();
assert!(input.is_some());
let input_value = input.unwrap();
assert_eq!(input_value["project_id"], 456);
assert_eq!(input_value["title"], "Feature request");
}
#[test]
fn operation_without_input_returns_none() {
let op = NoInputOp;
assert_eq!(op.input(), None);
}
#[tokio::test]
async fn operation_execute_returns_json_output() {
let op = GitLabIssueOp {
project_id: 789,
title: "Test".to_string(),
};
let result = op.execute().await;
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output["issue_id"], 42);
assert_eq!(output["project_id"], 789);
assert_eq!(output["title"], "Test");
}
#[tokio::test]
async fn operation_execute_can_return_error() {
let op = ErrorOp;
let result = op.execute().await;
assert!(result.is_err());
}
#[test]
fn operation_kind_identifies_different_types() {
let gitlab = GitLabIssueOp {
project_id: 1,
title: "a".to_string(),
};
let noop = NoInputOp;
let error = ErrorOp;
assert_eq!(gitlab.kind(), "gitlab");
assert_eq!(noop.kind(), "noop");
assert_eq!(error.kind(), "error_test");
}
#[tokio::test]
async fn no_input_operation_executes_successfully() {
let op = NoInputOp;
let result = op.execute().await;
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output["status"], "ok");
}
#[test]
fn gitlab_operation_input_has_all_fields() {
let op = GitLabIssueOp {
project_id: 999,
title: "Complete feature".to_string(),
};
let input = op.input().expect("input present");
assert!(input.is_object());
assert!(input.get("project_id").is_some());
assert!(input.get("title").is_some());
}
}