re-uploading work
This commit is contained in:
324
crates/api/src/dto/action.rs
Normal file
324
crates/api/src/dto/action.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
//! Action DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for creating a new action
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateActionRequest {
|
||||
/// Unique reference identifier (e.g., "core.http", "aws.ec2.start_instance")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference this action belongs to
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Post Message to Slack")]
|
||||
pub label: String,
|
||||
|
||||
/// Action description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Posts a message to a Slack channel")]
|
||||
pub description: String,
|
||||
|
||||
/// Entry point for action execution (e.g., path to script, function name)
|
||||
#[validate(length(min = 1, max = 1024))]
|
||||
#[schema(example = "/actions/slack/post_message.py")]
|
||||
pub entrypoint: String,
|
||||
|
||||
/// Optional runtime ID for this action
|
||||
#[schema(example = 1)]
|
||||
pub runtime: Option<i64>,
|
||||
|
||||
/// Parameter schema (JSON Schema) defining expected inputs
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"type": "object", "properties": {"channel": {"type": "string"}, "message": {"type": "string"}}}))]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema (JSON Schema) defining expected outputs
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"type": "object", "properties": {"message_id": {"type": "string"}}}))]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// Request DTO for updating an action
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateActionRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Post Message to Slack (Updated)")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Action description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Posts a message to a Slack channel with enhanced features")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Entry point for action execution
|
||||
#[validate(length(min = 1, max = 1024))]
|
||||
#[schema(example = "/actions/slack/post_message_v2.py")]
|
||||
pub entrypoint: Option<String>,
|
||||
|
||||
/// Runtime ID
|
||||
#[schema(example = 1)]
|
||||
pub runtime: Option<i64>,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// Response DTO for action information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ActionResponse {
|
||||
/// Action ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack ID
|
||||
#[schema(example = 1)]
|
||||
pub pack: i64,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Post Message to Slack")]
|
||||
pub label: String,
|
||||
|
||||
/// Action description
|
||||
#[schema(example = "Posts a message to a Slack channel")]
|
||||
pub description: String,
|
||||
|
||||
/// Entry point
|
||||
#[schema(example = "/actions/slack/post_message.py")]
|
||||
pub entrypoint: String,
|
||||
|
||||
/// Runtime ID
|
||||
#[schema(example = 1)]
|
||||
pub runtime: Option<i64>,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Whether this is an ad-hoc action (not from pack installation)
|
||||
#[schema(example = false)]
|
||||
pub is_adhoc: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified action response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ActionSummary {
|
||||
/// Action ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Post Message to Slack")]
|
||||
pub label: String,
|
||||
|
||||
/// Action description
|
||||
#[schema(example = "Posts a message to a Slack channel")]
|
||||
pub description: String,
|
||||
|
||||
/// Entry point
|
||||
#[schema(example = "/actions/slack/post_message.py")]
|
||||
pub entrypoint: String,
|
||||
|
||||
/// Runtime ID
|
||||
#[schema(example = 1)]
|
||||
pub runtime: Option<i64>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from Action model to ActionResponse
|
||||
impl From<attune_common::models::action::Action> for ActionResponse {
|
||||
fn from(action: attune_common::models::action::Action) -> Self {
|
||||
Self {
|
||||
id: action.id,
|
||||
r#ref: action.r#ref,
|
||||
pack: action.pack,
|
||||
pack_ref: action.pack_ref,
|
||||
label: action.label,
|
||||
description: action.description,
|
||||
entrypoint: action.entrypoint,
|
||||
runtime: action.runtime,
|
||||
param_schema: action.param_schema,
|
||||
out_schema: action.out_schema,
|
||||
is_adhoc: action.is_adhoc,
|
||||
created: action.created,
|
||||
updated: action.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Action model to ActionSummary
|
||||
impl From<attune_common::models::action::Action> for ActionSummary {
|
||||
fn from(action: attune_common::models::action::Action) -> Self {
|
||||
Self {
|
||||
id: action.id,
|
||||
r#ref: action.r#ref,
|
||||
pack_ref: action.pack_ref,
|
||||
label: action.label,
|
||||
description: action.description,
|
||||
entrypoint: action.entrypoint,
|
||||
runtime: action.runtime,
|
||||
created: action.created,
|
||||
updated: action.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response DTO for queue statistics
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct QueueStatsResponse {
|
||||
/// Action ID
|
||||
#[schema(example = 1)]
|
||||
pub action_id: i64,
|
||||
|
||||
/// Action reference
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Number of executions waiting in queue
|
||||
#[schema(example = 5)]
|
||||
pub queue_length: i32,
|
||||
|
||||
/// Number of currently running executions
|
||||
#[schema(example = 2)]
|
||||
pub active_count: i32,
|
||||
|
||||
/// Maximum concurrent executions allowed
|
||||
#[schema(example = 3)]
|
||||
pub max_concurrent: i32,
|
||||
|
||||
/// Timestamp of oldest queued execution (if any)
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub oldest_enqueued_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// Total executions enqueued since queue creation
|
||||
#[schema(example = 100)]
|
||||
pub total_enqueued: i64,
|
||||
|
||||
/// Total executions completed since queue creation
|
||||
#[schema(example = 95)]
|
||||
pub total_completed: i64,
|
||||
|
||||
/// Timestamp of last statistics update
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from QueueStats repository model to QueueStatsResponse
|
||||
impl From<attune_common::repositories::queue_stats::QueueStats> for QueueStatsResponse {
|
||||
fn from(stats: attune_common::repositories::queue_stats::QueueStats) -> Self {
|
||||
Self {
|
||||
action_id: stats.action_id,
|
||||
action_ref: String::new(), // Will be populated by the handler
|
||||
queue_length: stats.queue_length,
|
||||
active_count: stats.active_count,
|
||||
max_concurrent: stats.max_concurrent,
|
||||
oldest_enqueued_at: stats.oldest_enqueued_at,
|
||||
total_enqueued: stats.total_enqueued,
|
||||
total_completed: stats.total_completed,
|
||||
last_updated: stats.last_updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_action_request_validation() {
|
||||
let req = CreateActionRequest {
|
||||
r#ref: "".to_string(), // Invalid: empty
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
entrypoint: "/actions/test.py".to_string(),
|
||||
runtime: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_action_request_valid() {
|
||||
let req = CreateActionRequest {
|
||||
r#ref: "test.action".to_string(),
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Action".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
entrypoint: "/actions/test.py".to_string(),
|
||||
runtime: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_action_request_all_none() {
|
||||
let req = UpdateActionRequest {
|
||||
label: None,
|
||||
description: None,
|
||||
entrypoint: None,
|
||||
runtime: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
};
|
||||
|
||||
// Should be valid even with all None values
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
138
crates/api/src/dto/auth.rs
Normal file
138
crates/api/src/dto/auth.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
//! Authentication DTOs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Login request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
/// Identity login (username)
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "admin")]
|
||||
pub login: String,
|
||||
|
||||
/// Password
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "changeme123")]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Register request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct RegisterRequest {
|
||||
/// Identity login (username)
|
||||
#[validate(length(min = 3, max = 255))]
|
||||
#[schema(example = "newuser")]
|
||||
pub login: String,
|
||||
|
||||
/// Password
|
||||
#[validate(length(min = 8, max = 128))]
|
||||
#[schema(example = "SecurePass123!")]
|
||||
pub password: String,
|
||||
|
||||
/// Display name (optional)
|
||||
#[validate(length(max = 255))]
|
||||
#[schema(example = "New User")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Token response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TokenResponse {
|
||||
/// Access token (JWT)
|
||||
#[schema(example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")]
|
||||
pub access_token: String,
|
||||
|
||||
/// Refresh token
|
||||
#[schema(example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")]
|
||||
pub refresh_token: String,
|
||||
|
||||
/// Token type (always "Bearer")
|
||||
#[schema(example = "Bearer")]
|
||||
pub token_type: String,
|
||||
|
||||
/// Access token expiration in seconds
|
||||
#[schema(example = 3600)]
|
||||
pub expires_in: i64,
|
||||
|
||||
/// User information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<UserInfo>,
|
||||
}
|
||||
|
||||
/// User information included in token response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserInfo {
|
||||
/// Identity ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Identity login
|
||||
#[schema(example = "admin")]
|
||||
pub login: String,
|
||||
|
||||
/// Display name
|
||||
#[schema(example = "Administrator")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TokenResponse {
|
||||
pub fn new(access_token: String, refresh_token: String, expires_in: i64) -> Self {
|
||||
Self {
|
||||
access_token,
|
||||
refresh_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in,
|
||||
user: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_user(mut self, id: i64, login: String, display_name: Option<String>) -> Self {
|
||||
self.user = Some(UserInfo {
|
||||
id,
|
||||
login,
|
||||
display_name,
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh token request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct RefreshTokenRequest {
|
||||
/// Refresh token
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")]
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
/// Change password request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct ChangePasswordRequest {
|
||||
/// Current password
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "OldPassword123!")]
|
||||
pub current_password: String,
|
||||
|
||||
/// New password
|
||||
#[validate(length(min = 8, max = 128))]
|
||||
#[schema(example = "NewPassword456!")]
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
/// Current user response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CurrentUserResponse {
|
||||
/// Identity ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Identity login
|
||||
#[schema(example = "admin")]
|
||||
pub login: String,
|
||||
|
||||
/// Display name
|
||||
#[schema(example = "Administrator")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
221
crates/api/src/dto/common.rs
Normal file
221
crates/api/src/dto/common.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! Common DTO types used across all API endpoints
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
/// Pagination parameters for list endpoints
|
||||
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||||
pub struct PaginationParams {
|
||||
/// Page number (1-based)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Number of items per page
|
||||
#[serde(default = "default_page_size")]
|
||||
#[param(example = 50, minimum = 1, maximum = 100)]
|
||||
pub page_size: u32,
|
||||
}
|
||||
|
||||
fn default_page() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_page_size() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
impl PaginationParams {
|
||||
/// Get the SQL offset value
|
||||
pub fn offset(&self) -> u32 {
|
||||
(self.page.saturating_sub(1)) * self.page_size
|
||||
}
|
||||
|
||||
/// Get the SQL limit value
|
||||
pub fn limit(&self) -> u32 {
|
||||
self.page_size.min(100) // Max 100 items per page
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaginationParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page: default_page(),
|
||||
page_size: default_page_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paginated response wrapper
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PaginatedResponse<T> {
|
||||
/// The data items
|
||||
pub data: Vec<T>,
|
||||
|
||||
/// Pagination metadata
|
||||
pub pagination: PaginationMeta,
|
||||
}
|
||||
|
||||
/// Pagination metadata
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PaginationMeta {
|
||||
/// Current page number (1-based)
|
||||
#[schema(example = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Number of items per page
|
||||
#[schema(example = 50)]
|
||||
pub page_size: u32,
|
||||
|
||||
/// Total number of items
|
||||
#[schema(example = 150)]
|
||||
pub total_items: u64,
|
||||
|
||||
/// Total number of pages
|
||||
#[schema(example = 3)]
|
||||
pub total_pages: u32,
|
||||
}
|
||||
|
||||
impl PaginationMeta {
|
||||
/// Create pagination metadata
|
||||
pub fn new(page: u32, page_size: u32, total_items: u64) -> Self {
|
||||
let total_pages = if page_size > 0 {
|
||||
((total_items as f64) / (page_size as f64)).ceil() as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Self {
|
||||
page,
|
||||
page_size,
|
||||
total_items,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PaginatedResponse<T> {
|
||||
/// Create a new paginated response
|
||||
pub fn new(data: Vec<T>, params: &PaginationParams, total_items: u64) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pagination: PaginationMeta::new(params.page, params.page_size, total_items),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard API response wrapper
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ApiResponse<T> {
|
||||
/// Response data
|
||||
pub data: T,
|
||||
|
||||
/// Optional message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
/// Create a new API response
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
data,
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an API response with a message
|
||||
pub fn with_message(data: T, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
message: Some(message.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Success message response (for operations that don't return data)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct SuccessResponse {
|
||||
/// Success indicator
|
||||
#[schema(example = true)]
|
||||
pub success: bool,
|
||||
|
||||
/// Message describing the operation
|
||||
#[schema(example = "Operation completed successfully")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl SuccessResponse {
|
||||
/// Create a success response
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pagination_params_offset() {
|
||||
let params = PaginationParams {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
};
|
||||
assert_eq!(params.offset(), 0);
|
||||
|
||||
let params = PaginationParams {
|
||||
page: 2,
|
||||
page_size: 10,
|
||||
};
|
||||
assert_eq!(params.offset(), 10);
|
||||
|
||||
let params = PaginationParams {
|
||||
page: 3,
|
||||
page_size: 25,
|
||||
};
|
||||
assert_eq!(params.offset(), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pagination_params_limit() {
|
||||
let params = PaginationParams {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
};
|
||||
assert_eq!(params.limit(), 50);
|
||||
|
||||
// Should cap at 100
|
||||
let params = PaginationParams {
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
};
|
||||
assert_eq!(params.limit(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pagination_meta() {
|
||||
let meta = PaginationMeta::new(1, 10, 45);
|
||||
assert_eq!(meta.page, 1);
|
||||
assert_eq!(meta.page_size, 10);
|
||||
assert_eq!(meta.total_items, 45);
|
||||
assert_eq!(meta.total_pages, 5);
|
||||
|
||||
let meta = PaginationMeta::new(2, 20, 100);
|
||||
assert_eq!(meta.total_pages, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginated_response() {
|
||||
let data = vec![1, 2, 3, 4, 5];
|
||||
let params = PaginationParams::default();
|
||||
let response = PaginatedResponse::new(data.clone(), ¶ms, 100);
|
||||
|
||||
assert_eq!(response.data, data);
|
||||
assert_eq!(response.pagination.total_items, 100);
|
||||
assert_eq!(response.pagination.page, 1);
|
||||
}
|
||||
}
|
||||
344
crates/api/src/dto/event.rs
Normal file
344
crates/api/src/dto/event.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
//! Event and Enforcement data transfer objects
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use attune_common::models::{
|
||||
enums::{EnforcementCondition, EnforcementStatus},
|
||||
event::{Enforcement, Event},
|
||||
Id, JsonDict,
|
||||
};
|
||||
|
||||
/// Full event response with all details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventResponse {
|
||||
/// Event ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub trigger: Option<Id>,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "core.webhook")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Event configuration
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub config: Option<JsonDict>,
|
||||
|
||||
/// Event payload data
|
||||
#[schema(value_type = Object, example = json!({"url": "/webhook", "method": "POST"}))]
|
||||
pub payload: Option<JsonDict>,
|
||||
|
||||
/// Source ID (sensor that generated this event)
|
||||
#[schema(example = 1)]
|
||||
pub source: Option<Id>,
|
||||
|
||||
/// Source reference
|
||||
#[schema(example = "monitoring.webhook_sensor")]
|
||||
pub source_ref: Option<String>,
|
||||
|
||||
/// Rule ID (if event was generated by a specific rule)
|
||||
#[schema(example = 1)]
|
||||
pub rule: Option<Id>,
|
||||
|
||||
/// Rule reference (if event was generated by a specific rule)
|
||||
#[schema(example = "core.timer_rule")]
|
||||
pub rule_ref: Option<String>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Event> for EventResponse {
|
||||
fn from(event: Event) -> Self {
|
||||
Self {
|
||||
id: event.id,
|
||||
trigger: event.trigger,
|
||||
trigger_ref: event.trigger_ref,
|
||||
config: event.config,
|
||||
payload: event.payload,
|
||||
source: event.source,
|
||||
source_ref: event.source_ref,
|
||||
rule: event.rule,
|
||||
rule_ref: event.rule_ref,
|
||||
created: event.created,
|
||||
updated: event.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary event response for list views
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EventSummary {
|
||||
/// Event ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub trigger: Option<Id>,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "core.webhook")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Source ID
|
||||
#[schema(example = 1)]
|
||||
pub source: Option<Id>,
|
||||
|
||||
/// Source reference
|
||||
#[schema(example = "monitoring.webhook_sensor")]
|
||||
pub source_ref: Option<String>,
|
||||
|
||||
/// Rule ID (if event was generated by a specific rule)
|
||||
#[schema(example = 1)]
|
||||
pub rule: Option<Id>,
|
||||
|
||||
/// Rule reference (if event was generated by a specific rule)
|
||||
#[schema(example = "core.timer_rule")]
|
||||
pub rule_ref: Option<String>,
|
||||
|
||||
/// Whether event has payload data
|
||||
#[schema(example = true)]
|
||||
pub has_payload: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Event> for EventSummary {
|
||||
fn from(event: Event) -> Self {
|
||||
Self {
|
||||
id: event.id,
|
||||
trigger: event.trigger,
|
||||
trigger_ref: event.trigger_ref,
|
||||
source: event.source,
|
||||
source_ref: event.source_ref,
|
||||
rule: event.rule,
|
||||
rule_ref: event.rule_ref,
|
||||
has_payload: event.payload.is_some(),
|
||||
created: event.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query parameters for filtering events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)]
|
||||
pub struct EventQueryParams {
|
||||
/// Filter by trigger ID
|
||||
#[param(example = 1)]
|
||||
pub trigger: Option<Id>,
|
||||
|
||||
/// Filter by trigger reference
|
||||
#[param(example = "core.webhook")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// Filter by source ID
|
||||
#[param(example = 1)]
|
||||
pub source: Option<Id>,
|
||||
|
||||
/// Page number (1-indexed)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Items per page
|
||||
#[serde(default = "default_per_page")]
|
||||
#[param(example = 50, minimum = 1, maximum = 100)]
|
||||
pub per_page: u32,
|
||||
}
|
||||
|
||||
fn default_page() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_per_page() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
impl EventQueryParams {
|
||||
/// Get the offset for pagination
|
||||
pub fn offset(&self) -> u32 {
|
||||
(self.page - 1) * self.per_page
|
||||
}
|
||||
|
||||
/// Get the limit for pagination
|
||||
pub fn limit(&self) -> u32 {
|
||||
self.per_page
|
||||
}
|
||||
}
|
||||
|
||||
/// Full enforcement response with all details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EnforcementResponse {
|
||||
/// Enforcement ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Rule ID
|
||||
#[schema(example = 1)]
|
||||
pub rule: Option<Id>,
|
||||
|
||||
/// Rule reference
|
||||
#[schema(example = "slack.notify_on_error")]
|
||||
pub rule_ref: String,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "system.error_event")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Enforcement configuration
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub config: Option<JsonDict>,
|
||||
|
||||
/// Event ID that triggered this enforcement
|
||||
#[schema(example = 1)]
|
||||
pub event: Option<Id>,
|
||||
|
||||
/// Enforcement status
|
||||
#[schema(example = "succeeded")]
|
||||
pub status: EnforcementStatus,
|
||||
|
||||
/// Enforcement payload
|
||||
#[schema(value_type = Object)]
|
||||
pub payload: JsonDict,
|
||||
|
||||
/// Enforcement condition
|
||||
#[schema(example = "matched")]
|
||||
pub condition: EnforcementCondition,
|
||||
|
||||
/// Enforcement conditions (rule evaluation criteria)
|
||||
#[schema(value_type = Object)]
|
||||
pub conditions: JsonValue,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Enforcement> for EnforcementResponse {
|
||||
fn from(enforcement: Enforcement) -> Self {
|
||||
Self {
|
||||
id: enforcement.id,
|
||||
rule: enforcement.rule,
|
||||
rule_ref: enforcement.rule_ref,
|
||||
trigger_ref: enforcement.trigger_ref,
|
||||
config: enforcement.config,
|
||||
event: enforcement.event,
|
||||
status: enforcement.status,
|
||||
payload: enforcement.payload,
|
||||
condition: enforcement.condition,
|
||||
conditions: enforcement.conditions,
|
||||
created: enforcement.created,
|
||||
updated: enforcement.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary enforcement response for list views
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EnforcementSummary {
|
||||
/// Enforcement ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Rule ID
|
||||
#[schema(example = 1)]
|
||||
pub rule: Option<Id>,
|
||||
|
||||
/// Rule reference
|
||||
#[schema(example = "slack.notify_on_error")]
|
||||
pub rule_ref: String,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "system.error_event")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Event ID
|
||||
#[schema(example = 1)]
|
||||
pub event: Option<Id>,
|
||||
|
||||
/// Enforcement status
|
||||
#[schema(example = "succeeded")]
|
||||
pub status: EnforcementStatus,
|
||||
|
||||
/// Enforcement condition
|
||||
#[schema(example = "matched")]
|
||||
pub condition: EnforcementCondition,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Enforcement> for EnforcementSummary {
|
||||
fn from(enforcement: Enforcement) -> Self {
|
||||
Self {
|
||||
id: enforcement.id,
|
||||
rule: enforcement.rule,
|
||||
rule_ref: enforcement.rule_ref,
|
||||
trigger_ref: enforcement.trigger_ref,
|
||||
event: enforcement.event,
|
||||
status: enforcement.status,
|
||||
condition: enforcement.condition,
|
||||
created: enforcement.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query parameters for filtering enforcements
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)]
|
||||
pub struct EnforcementQueryParams {
|
||||
/// Filter by rule ID
|
||||
#[param(example = 1)]
|
||||
pub rule: Option<Id>,
|
||||
|
||||
/// Filter by event ID
|
||||
#[param(example = 1)]
|
||||
pub event: Option<Id>,
|
||||
|
||||
/// Filter by status
|
||||
#[param(example = "success")]
|
||||
pub status: Option<EnforcementStatus>,
|
||||
|
||||
/// Filter by trigger reference
|
||||
#[param(example = "core.webhook")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// Page number (1-indexed)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Items per page
|
||||
#[serde(default = "default_per_page")]
|
||||
#[param(example = 50, minimum = 1, maximum = 100)]
|
||||
pub per_page: u32,
|
||||
}
|
||||
|
||||
impl EnforcementQueryParams {
|
||||
/// Get the offset for pagination
|
||||
pub fn offset(&self) -> u32 {
|
||||
(self.page - 1) * self.per_page
|
||||
}
|
||||
|
||||
/// Get the limit for pagination
|
||||
pub fn limit(&self) -> u32 {
|
||||
self.per_page
|
||||
}
|
||||
}
|
||||
283
crates/api/src/dto/execution.rs
Normal file
283
crates/api/src/dto/execution.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
//! Execution DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use attune_common::models::enums::ExecutionStatus;
|
||||
|
||||
/// Request DTO for creating a manual execution
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct CreateExecutionRequest {
|
||||
/// Action reference to execute
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Execution parameters/configuration
|
||||
#[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "Manual test"}))]
|
||||
pub parameters: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// Response DTO for execution information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ExecutionResponse {
|
||||
/// Execution ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Action ID (optional, may be null for ad-hoc executions)
|
||||
#[schema(example = 1)]
|
||||
pub action: Option<i64>,
|
||||
|
||||
/// Action reference
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Execution configuration/parameters
|
||||
#[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "System error detected"}))]
|
||||
pub config: Option<JsonValue>,
|
||||
|
||||
/// Parent execution ID (for nested/child executions)
|
||||
#[schema(example = 1)]
|
||||
pub parent: Option<i64>,
|
||||
|
||||
/// Enforcement ID (rule enforcement that triggered this)
|
||||
#[schema(example = 1)]
|
||||
pub enforcement: Option<i64>,
|
||||
|
||||
/// Executor ID (worker/executor that ran this)
|
||||
#[schema(example = 1)]
|
||||
pub executor: Option<i64>,
|
||||
|
||||
/// Execution status
|
||||
#[schema(example = "succeeded")]
|
||||
pub status: ExecutionStatus,
|
||||
|
||||
/// Execution result/output
|
||||
#[schema(value_type = Object, example = json!({"message_id": "1234567890.123456"}))]
|
||||
pub result: Option<JsonValue>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:35:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified execution response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct ExecutionSummary {
|
||||
/// Execution ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Action reference
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Execution status
|
||||
#[schema(example = "succeeded")]
|
||||
pub status: ExecutionStatus,
|
||||
|
||||
/// Parent execution ID
|
||||
#[schema(example = 1)]
|
||||
pub parent: Option<i64>,
|
||||
|
||||
/// Enforcement ID
|
||||
#[schema(example = 1)]
|
||||
pub enforcement: Option<i64>,
|
||||
|
||||
/// Rule reference (if triggered by a rule)
|
||||
#[schema(example = "core.on_timer")]
|
||||
pub rule_ref: Option<String>,
|
||||
|
||||
/// Trigger reference (if triggered by a trigger)
|
||||
#[schema(example = "core.timer")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:35:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Query parameters for filtering executions
|
||||
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||||
pub struct ExecutionQueryParams {
|
||||
/// Filter by execution status
|
||||
#[param(example = "succeeded")]
|
||||
pub status: Option<ExecutionStatus>,
|
||||
|
||||
/// Filter by action reference
|
||||
#[param(example = "slack.post_message")]
|
||||
pub action_ref: Option<String>,
|
||||
|
||||
/// Filter by pack name
|
||||
#[param(example = "core")]
|
||||
pub pack_name: Option<String>,
|
||||
|
||||
/// Filter by rule reference
|
||||
#[param(example = "core.on_timer")]
|
||||
pub rule_ref: Option<String>,
|
||||
|
||||
/// Filter by trigger reference
|
||||
#[param(example = "core.timer")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// Filter by executor ID
|
||||
#[param(example = 1)]
|
||||
pub executor: Option<i64>,
|
||||
|
||||
/// Search in result JSON (case-insensitive substring match)
|
||||
#[param(example = "error")]
|
||||
pub result_contains: Option<String>,
|
||||
|
||||
/// Filter by enforcement ID
|
||||
#[param(example = 1)]
|
||||
pub enforcement: Option<i64>,
|
||||
|
||||
/// Filter by parent execution ID
|
||||
#[param(example = 1)]
|
||||
pub parent: Option<i64>,
|
||||
|
||||
/// Page number (for pagination)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Items per page (for pagination)
|
||||
#[serde(default = "default_per_page")]
|
||||
#[param(example = 50, minimum = 1, maximum = 100)]
|
||||
pub per_page: u32,
|
||||
}
|
||||
|
||||
impl ExecutionQueryParams {
|
||||
/// Get the SQL offset value
|
||||
pub fn offset(&self) -> u32 {
|
||||
(self.page.saturating_sub(1)) * self.per_page
|
||||
}
|
||||
|
||||
/// Get the limit value (with max cap)
|
||||
pub fn limit(&self) -> u32 {
|
||||
self.per_page.min(100)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Execution model to ExecutionResponse
|
||||
impl From<attune_common::models::execution::Execution> for ExecutionResponse {
|
||||
fn from(execution: attune_common::models::execution::Execution) -> Self {
|
||||
Self {
|
||||
id: execution.id,
|
||||
action: execution.action,
|
||||
action_ref: execution.action_ref,
|
||||
config: execution
|
||||
.config
|
||||
.map(|c| serde_json::to_value(c).unwrap_or(JsonValue::Null)),
|
||||
parent: execution.parent,
|
||||
enforcement: execution.enforcement,
|
||||
executor: execution.executor,
|
||||
status: execution.status,
|
||||
result: execution
|
||||
.result
|
||||
.map(|r| serde_json::to_value(r).unwrap_or(JsonValue::Null)),
|
||||
created: execution.created,
|
||||
updated: execution.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Execution model to ExecutionSummary
|
||||
impl From<attune_common::models::execution::Execution> for ExecutionSummary {
|
||||
fn from(execution: attune_common::models::execution::Execution) -> Self {
|
||||
Self {
|
||||
id: execution.id,
|
||||
action_ref: execution.action_ref,
|
||||
status: execution.status,
|
||||
parent: execution.parent,
|
||||
enforcement: execution.enforcement,
|
||||
rule_ref: None, // Populated separately via enforcement lookup
|
||||
trigger_ref: None, // Populated separately via enforcement lookup
|
||||
created: execution.created,
|
||||
updated: execution.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_page() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_per_page() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_params_defaults() {
|
||||
let json = r#"{}"#;
|
||||
let params: ExecutionQueryParams = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(params.page, 1);
|
||||
assert_eq!(params.per_page, 20);
|
||||
assert!(params.status.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_params_with_filters() {
|
||||
let json = r#"{
|
||||
"status": "completed",
|
||||
"action_ref": "test.action",
|
||||
"page": 2,
|
||||
"per_page": 50
|
||||
}"#;
|
||||
let params: ExecutionQueryParams = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(params.page, 2);
|
||||
assert_eq!(params.per_page, 50);
|
||||
assert_eq!(params.status, Some(ExecutionStatus::Completed));
|
||||
assert_eq!(params.action_ref, Some("test.action".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_params_offset() {
|
||||
let params = ExecutionQueryParams {
|
||||
status: None,
|
||||
action_ref: None,
|
||||
enforcement: None,
|
||||
parent: None,
|
||||
pack_name: None,
|
||||
rule_ref: None,
|
||||
trigger_ref: None,
|
||||
executor: None,
|
||||
result_contains: None,
|
||||
page: 3,
|
||||
per_page: 20,
|
||||
};
|
||||
assert_eq!(params.offset(), 40); // (3-1) * 20
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_params_limit_cap() {
|
||||
let params = ExecutionQueryParams {
|
||||
status: None,
|
||||
action_ref: None,
|
||||
enforcement: None,
|
||||
parent: None,
|
||||
pack_name: None,
|
||||
rule_ref: None,
|
||||
trigger_ref: None,
|
||||
executor: None,
|
||||
result_contains: None,
|
||||
page: 1,
|
||||
per_page: 200, // Exceeds max
|
||||
};
|
||||
assert_eq!(params.limit(), 100); // Capped at 100
|
||||
}
|
||||
}
|
||||
215
crates/api/src/dto/inquiry.rs
Normal file
215
crates/api/src/dto/inquiry.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
//! Inquiry data transfer objects
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use validator::Validate;
|
||||
|
||||
use attune_common::models::{enums::InquiryStatus, inquiry::Inquiry, Id, JsonDict, JsonSchema};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
/// Full inquiry response with all details
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct InquiryResponse {
|
||||
/// Inquiry ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Execution ID this inquiry belongs to
|
||||
#[schema(example = 1)]
|
||||
pub execution: Id,
|
||||
|
||||
/// Prompt text displayed to the user
|
||||
#[schema(example = "Approve deployment to production?")]
|
||||
pub prompt: String,
|
||||
|
||||
/// JSON schema for expected response
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub response_schema: Option<JsonSchema>,
|
||||
|
||||
/// Identity ID this inquiry is assigned to
|
||||
#[schema(example = 1)]
|
||||
pub assigned_to: Option<Id>,
|
||||
|
||||
/// Current status of the inquiry
|
||||
#[schema(example = "pending")]
|
||||
pub status: InquiryStatus,
|
||||
|
||||
/// Response data provided by the user
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub response: Option<JsonDict>,
|
||||
|
||||
/// When the inquiry expires
|
||||
#[schema(example = "2024-01-13T11:30:00Z")]
|
||||
pub timeout_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// When the inquiry was responded to
|
||||
#[schema(example = "2024-01-13T10:45:00Z")]
|
||||
pub responded_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:45:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Inquiry> for InquiryResponse {
|
||||
fn from(inquiry: Inquiry) -> Self {
|
||||
Self {
|
||||
id: inquiry.id,
|
||||
execution: inquiry.execution,
|
||||
prompt: inquiry.prompt,
|
||||
response_schema: inquiry.response_schema,
|
||||
assigned_to: inquiry.assigned_to,
|
||||
status: inquiry.status,
|
||||
response: inquiry.response,
|
||||
timeout_at: inquiry.timeout_at,
|
||||
responded_at: inquiry.responded_at,
|
||||
created: inquiry.created,
|
||||
updated: inquiry.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary inquiry response for list views
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct InquirySummary {
|
||||
/// Inquiry ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Execution ID
|
||||
#[schema(example = 1)]
|
||||
pub execution: Id,
|
||||
|
||||
/// Prompt text
|
||||
#[schema(example = "Approve deployment to production?")]
|
||||
pub prompt: String,
|
||||
|
||||
/// Assigned identity ID
|
||||
#[schema(example = 1)]
|
||||
pub assigned_to: Option<Id>,
|
||||
|
||||
/// Inquiry status
|
||||
#[schema(example = "pending")]
|
||||
pub status: InquiryStatus,
|
||||
|
||||
/// Whether a response has been provided
|
||||
#[schema(example = false)]
|
||||
pub has_response: bool,
|
||||
|
||||
/// Timeout timestamp
|
||||
#[schema(example = "2024-01-13T11:30:00Z")]
|
||||
pub timeout_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Inquiry> for InquirySummary {
|
||||
fn from(inquiry: Inquiry) -> Self {
|
||||
Self {
|
||||
id: inquiry.id,
|
||||
execution: inquiry.execution,
|
||||
prompt: inquiry.prompt,
|
||||
assigned_to: inquiry.assigned_to,
|
||||
status: inquiry.status,
|
||||
has_response: inquiry.response.is_some(),
|
||||
timeout_at: inquiry.timeout_at,
|
||||
created: inquiry.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to create a new inquiry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateInquiryRequest {
|
||||
/// Execution ID this inquiry belongs to
|
||||
#[schema(example = 1)]
|
||||
pub execution: Id,
|
||||
|
||||
/// Prompt text to display to the user
|
||||
#[validate(length(min = 1, max = 10000))]
|
||||
#[schema(example = "Approve deployment to production?")]
|
||||
pub prompt: String,
|
||||
|
||||
/// Optional JSON schema for the expected response format
|
||||
#[schema(value_type = Object, example = json!({"type": "object", "properties": {"approved": {"type": "boolean"}}}))]
|
||||
pub response_schema: Option<JsonSchema>,
|
||||
|
||||
/// Optional identity ID to assign this inquiry to
|
||||
#[schema(example = 1)]
|
||||
pub assigned_to: Option<Id>,
|
||||
|
||||
/// Optional timeout timestamp (when inquiry expires)
|
||||
#[schema(example = "2024-01-13T11:30:00Z")]
|
||||
pub timeout_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Request to update an inquiry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateInquiryRequest {
|
||||
/// Update the inquiry status
|
||||
#[schema(example = "responded")]
|
||||
pub status: Option<InquiryStatus>,
|
||||
|
||||
/// Update the response data
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub response: Option<JsonDict>,
|
||||
|
||||
/// Update the assigned_to identity
|
||||
#[schema(example = 2)]
|
||||
pub assigned_to: Option<Id>,
|
||||
}
|
||||
|
||||
/// Request to respond to an inquiry (user-facing endpoint)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct InquiryRespondRequest {
|
||||
/// Response data conforming to the inquiry's response_schema
|
||||
#[schema(value_type = Object)]
|
||||
pub response: JsonValue,
|
||||
}
|
||||
|
||||
/// Query parameters for filtering inquiries
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)]
|
||||
pub struct InquiryQueryParams {
|
||||
/// Filter by status
|
||||
#[param(example = "pending")]
|
||||
pub status: Option<InquiryStatus>,
|
||||
|
||||
/// Filter by execution ID
|
||||
#[param(example = 1)]
|
||||
pub execution: Option<Id>,
|
||||
|
||||
/// Filter by assigned identity
|
||||
#[param(example = 1)]
|
||||
pub assigned_to: Option<Id>,
|
||||
|
||||
/// Pagination offset
|
||||
#[param(example = 0)]
|
||||
pub offset: Option<usize>,
|
||||
|
||||
/// Pagination limit
|
||||
#[param(example = 50)]
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Paginated list response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListResponse<T> {
|
||||
/// List of items
|
||||
pub data: Vec<T>,
|
||||
|
||||
/// Total count of items (before pagination)
|
||||
pub total: usize,
|
||||
|
||||
/// Offset used for this page
|
||||
pub offset: usize,
|
||||
|
||||
/// Limit used for this page
|
||||
pub limit: usize,
|
||||
}
|
||||
270
crates/api/src/dto/key.rs
Normal file
270
crates/api/src/dto/key.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
//! Key/Secret data transfer objects
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use validator::Validate;
|
||||
|
||||
use attune_common::models::{key::Key, Id, OwnerType};
|
||||
|
||||
/// Full key response with all details (value redacted in list views)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct KeyResponse {
|
||||
/// Unique key ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "github_token")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Type of owner
|
||||
pub owner_type: OwnerType,
|
||||
|
||||
/// Owner identifier
|
||||
#[schema(example = "github-integration")]
|
||||
pub owner: Option<String>,
|
||||
|
||||
/// Owner identity ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_identity: Option<Id>,
|
||||
|
||||
/// Owner pack ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_pack: Option<Id>,
|
||||
|
||||
/// Owner pack reference
|
||||
#[schema(example = "github")]
|
||||
pub owner_pack_ref: Option<String>,
|
||||
|
||||
/// Owner action ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_action: Option<Id>,
|
||||
|
||||
/// Owner action reference
|
||||
#[schema(example = "github.create_issue")]
|
||||
pub owner_action_ref: Option<String>,
|
||||
|
||||
/// Owner sensor ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_sensor: Option<Id>,
|
||||
|
||||
/// Owner sensor reference
|
||||
#[schema(example = "github.webhook")]
|
||||
pub owner_sensor_ref: Option<String>,
|
||||
|
||||
/// Human-readable name
|
||||
#[schema(example = "GitHub API Token")]
|
||||
pub name: String,
|
||||
|
||||
/// Whether the value is encrypted
|
||||
#[schema(example = true)]
|
||||
pub encrypted: bool,
|
||||
|
||||
/// The secret value (decrypted if encrypted)
|
||||
#[schema(example = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")]
|
||||
pub value: String,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Key> for KeyResponse {
|
||||
fn from(key: Key) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
r#ref: key.r#ref,
|
||||
owner_type: key.owner_type,
|
||||
owner: key.owner,
|
||||
owner_identity: key.owner_identity,
|
||||
owner_pack: key.owner_pack,
|
||||
owner_pack_ref: key.owner_pack_ref,
|
||||
owner_action: key.owner_action,
|
||||
owner_action_ref: key.owner_action_ref,
|
||||
owner_sensor: key.owner_sensor,
|
||||
owner_sensor_ref: key.owner_sensor_ref,
|
||||
name: key.name,
|
||||
encrypted: key.encrypted,
|
||||
value: key.value,
|
||||
created: key.created,
|
||||
updated: key.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary key response for list views (value redacted)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct KeySummary {
|
||||
/// Unique key ID
|
||||
#[schema(example = 1)]
|
||||
pub id: Id,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "github_token")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Type of owner
|
||||
pub owner_type: OwnerType,
|
||||
|
||||
/// Owner identifier
|
||||
#[schema(example = "github-integration")]
|
||||
pub owner: Option<String>,
|
||||
|
||||
/// Human-readable name
|
||||
#[schema(example = "GitHub API Token")]
|
||||
pub name: String,
|
||||
|
||||
/// Whether the value is encrypted
|
||||
#[schema(example = true)]
|
||||
pub encrypted: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Key> for KeySummary {
|
||||
fn from(key: Key) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
r#ref: key.r#ref,
|
||||
owner_type: key.owner_type,
|
||||
owner: key.owner,
|
||||
name: key.name,
|
||||
encrypted: key.encrypted,
|
||||
created: key.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to create a new key/secret
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateKeyRequest {
|
||||
/// Unique reference for the key (e.g., "github_token", "aws_secret_key")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "github_token")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Type of owner (system, identity, pack, action, sensor)
|
||||
pub owner_type: OwnerType,
|
||||
|
||||
/// Optional owner string identifier
|
||||
#[validate(length(max = 255))]
|
||||
#[schema(example = "github-integration")]
|
||||
pub owner: Option<String>,
|
||||
|
||||
/// Optional owner identity ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_identity: Option<Id>,
|
||||
|
||||
/// Optional owner pack ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_pack: Option<Id>,
|
||||
|
||||
/// Optional owner pack reference
|
||||
#[validate(length(max = 255))]
|
||||
#[schema(example = "github")]
|
||||
pub owner_pack_ref: Option<String>,
|
||||
|
||||
/// Optional owner action ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_action: Option<Id>,
|
||||
|
||||
/// Optional owner action reference
|
||||
#[validate(length(max = 255))]
|
||||
#[schema(example = "github.create_issue")]
|
||||
pub owner_action_ref: Option<String>,
|
||||
|
||||
/// Optional owner sensor ID
|
||||
#[schema(example = 1)]
|
||||
pub owner_sensor: Option<Id>,
|
||||
|
||||
/// Optional owner sensor reference
|
||||
#[validate(length(max = 255))]
|
||||
#[schema(example = "github.webhook")]
|
||||
pub owner_sensor_ref: Option<String>,
|
||||
|
||||
/// Human-readable name for the key
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "GitHub API Token")]
|
||||
pub name: String,
|
||||
|
||||
/// The secret value to store
|
||||
#[validate(length(min = 1, max = 10000))]
|
||||
#[schema(example = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")]
|
||||
pub value: String,
|
||||
|
||||
/// Whether to encrypt the value (recommended: true)
|
||||
#[serde(default = "default_encrypted")]
|
||||
#[schema(example = true)]
|
||||
pub encrypted: bool,
|
||||
}
|
||||
|
||||
fn default_encrypted() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Request to update an existing key/secret
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateKeyRequest {
|
||||
/// Update the human-readable name
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "GitHub API Token (Updated)")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Update the secret value
|
||||
#[validate(length(min = 1, max = 10000))]
|
||||
#[schema(example = "ghp_new_token_xxxxxxxxxxxxxxxxxxxxxxxx")]
|
||||
pub value: Option<String>,
|
||||
|
||||
/// Update encryption status (re-encrypts if changing from false to true)
|
||||
#[schema(example = true)]
|
||||
pub encrypted: Option<bool>,
|
||||
}
|
||||
|
||||
/// Query parameters for filtering keys
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)]
|
||||
pub struct KeyQueryParams {
|
||||
/// Filter by owner type
|
||||
#[param(example = "pack")]
|
||||
pub owner_type: Option<OwnerType>,
|
||||
|
||||
/// Filter by owner string
|
||||
#[param(example = "github-integration")]
|
||||
pub owner: Option<String>,
|
||||
|
||||
/// Page number (1-indexed)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
pub page: u32,
|
||||
|
||||
/// Items per page
|
||||
#[serde(default = "default_per_page")]
|
||||
#[param(example = 50, minimum = 1, maximum = 100)]
|
||||
pub per_page: u32,
|
||||
}
|
||||
|
||||
fn default_page() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_per_page() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
impl KeyQueryParams {
|
||||
/// Get the offset for pagination
|
||||
pub fn offset(&self) -> u32 {
|
||||
(self.page - 1) * self.per_page
|
||||
}
|
||||
|
||||
/// Get the limit for pagination
|
||||
pub fn limit(&self) -> u32 {
|
||||
self.per_page
|
||||
}
|
||||
}
|
||||
44
crates/api/src/dto/mod.rs
Normal file
44
crates/api/src/dto/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Data Transfer Objects (DTOs) for API requests and responses
|
||||
|
||||
pub mod action;
|
||||
pub mod auth;
|
||||
pub mod common;
|
||||
pub mod event;
|
||||
pub mod execution;
|
||||
pub mod inquiry;
|
||||
pub mod key;
|
||||
pub mod pack;
|
||||
pub mod rule;
|
||||
pub mod trigger;
|
||||
pub mod webhook;
|
||||
pub mod workflow;
|
||||
|
||||
pub use action::{ActionResponse, ActionSummary, CreateActionRequest, UpdateActionRequest};
|
||||
pub use auth::{
|
||||
ChangePasswordRequest, CurrentUserResponse, LoginRequest, RefreshTokenRequest, RegisterRequest,
|
||||
TokenResponse,
|
||||
};
|
||||
pub use common::{
|
||||
ApiResponse, PaginatedResponse, PaginationMeta, PaginationParams, SuccessResponse,
|
||||
};
|
||||
pub use event::{
|
||||
EnforcementQueryParams, EnforcementResponse, EnforcementSummary, EventQueryParams,
|
||||
EventResponse, EventSummary,
|
||||
};
|
||||
pub use execution::{CreateExecutionRequest, ExecutionQueryParams, ExecutionResponse, ExecutionSummary};
|
||||
pub use inquiry::{
|
||||
CreateInquiryRequest, InquiryQueryParams, InquiryRespondRequest, InquiryResponse,
|
||||
InquirySummary, UpdateInquiryRequest,
|
||||
};
|
||||
pub use key::{CreateKeyRequest, KeyQueryParams, KeyResponse, KeySummary, UpdateKeyRequest};
|
||||
pub use pack::{CreatePackRequest, PackResponse, PackSummary, UpdatePackRequest};
|
||||
pub use rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest};
|
||||
pub use trigger::{
|
||||
CreateSensorRequest, CreateTriggerRequest, SensorResponse, SensorSummary, TriggerResponse,
|
||||
TriggerSummary, UpdateSensorRequest, UpdateTriggerRequest,
|
||||
};
|
||||
pub use webhook::{WebhookReceiverRequest, WebhookReceiverResponse};
|
||||
pub use workflow::{
|
||||
CreateWorkflowRequest, UpdateWorkflowRequest, WorkflowResponse, WorkflowSearchParams,
|
||||
WorkflowSummary,
|
||||
};
|
||||
381
crates/api/src/dto/pack.rs
Normal file
381
crates/api/src/dto/pack.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
//! Pack DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for creating a new pack
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreatePackRequest {
|
||||
/// Unique reference identifier (e.g., "core", "aws", "slack")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Slack Integration")]
|
||||
pub label: String,
|
||||
|
||||
/// Pack description
|
||||
#[schema(example = "Integration with Slack for messaging and notifications")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Pack version (semver format recommended)
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Configuration schema (JSON Schema)
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"type": "object", "properties": {"api_token": {"type": "string"}}}))]
|
||||
pub conf_schema: JsonValue,
|
||||
|
||||
/// Pack configuration values
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"api_token": "xoxb-..."}))]
|
||||
pub config: JsonValue,
|
||||
|
||||
/// Pack metadata
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"author": "Attune Team"}))]
|
||||
pub meta: JsonValue,
|
||||
|
||||
/// Tags for categorization
|
||||
#[serde(default)]
|
||||
#[schema(example = json!(["messaging", "collaboration"]))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Runtime dependencies (refs of required packs)
|
||||
#[serde(default)]
|
||||
#[schema(example = json!(["core"]))]
|
||||
pub runtime_deps: Vec<String>,
|
||||
|
||||
/// Whether this is a standard/built-in pack
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub is_standard: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for registering a pack from local filesystem
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct RegisterPackRequest {
|
||||
/// Local filesystem path to the pack directory
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "/path/to/packs/mypack")]
|
||||
pub path: String,
|
||||
|
||||
/// Skip running pack tests during registration
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_tests: bool,
|
||||
|
||||
/// Force registration even if tests fail
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for installing a pack from remote source
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct InstallPackRequest {
|
||||
/// Repository URL or source location
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "https://github.com/attune/pack-slack.git")]
|
||||
pub source: String,
|
||||
|
||||
/// Git branch, tag, or commit reference
|
||||
#[schema(example = "main")]
|
||||
pub ref_spec: Option<String>,
|
||||
|
||||
/// Force reinstall if pack already exists
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub force: bool,
|
||||
|
||||
/// Skip running pack tests during installation
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_tests: bool,
|
||||
|
||||
/// Skip dependency validation (not recommended)
|
||||
#[serde(default)]
|
||||
#[schema(example = false)]
|
||||
pub skip_deps: bool,
|
||||
}
|
||||
|
||||
/// Response for pack install/register operations with test results
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackInstallResponse {
|
||||
/// The installed/registered pack
|
||||
pub pack: PackResponse,
|
||||
|
||||
/// Test execution result (if tests were run)
|
||||
pub test_result: Option<attune_common::models::pack_test::PackTestResult>,
|
||||
|
||||
/// Whether tests were skipped
|
||||
pub tests_skipped: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for updating a pack
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdatePackRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Slack Integration v2")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Pack description
|
||||
#[schema(example = "Enhanced Slack integration with new features")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Pack version
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
#[schema(example = "2.0.0")]
|
||||
pub version: Option<String>,
|
||||
|
||||
/// Configuration schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub conf_schema: Option<JsonValue>,
|
||||
|
||||
/// Pack configuration values
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub config: Option<JsonValue>,
|
||||
|
||||
/// Pack metadata
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub meta: Option<JsonValue>,
|
||||
|
||||
/// Tags for categorization
|
||||
#[schema(example = json!(["messaging", "collaboration", "webhooks"]))]
|
||||
pub tags: Option<Vec<String>>,
|
||||
|
||||
/// Runtime dependencies
|
||||
#[schema(example = json!(["core", "http"]))]
|
||||
pub runtime_deps: Option<Vec<String>>,
|
||||
|
||||
/// Whether this is a standard pack
|
||||
#[schema(example = false)]
|
||||
pub is_standard: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response DTO for pack information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackResponse {
|
||||
/// Pack ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Slack Integration")]
|
||||
pub label: String,
|
||||
|
||||
/// Pack description
|
||||
#[schema(example = "Integration with Slack for messaging and notifications")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Pack version
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Configuration schema
|
||||
#[schema(value_type = Object)]
|
||||
pub conf_schema: JsonValue,
|
||||
|
||||
/// Pack configuration
|
||||
#[schema(value_type = Object)]
|
||||
pub config: JsonValue,
|
||||
|
||||
/// Pack metadata
|
||||
#[schema(value_type = Object)]
|
||||
pub meta: JsonValue,
|
||||
|
||||
/// Tags
|
||||
#[schema(example = json!(["messaging", "collaboration"]))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Runtime dependencies
|
||||
#[schema(example = json!(["core"]))]
|
||||
pub runtime_deps: Vec<String>,
|
||||
|
||||
/// Is standard pack
|
||||
#[schema(example = false)]
|
||||
pub is_standard: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified pack response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackSummary {
|
||||
/// Pack ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Slack Integration")]
|
||||
pub label: String,
|
||||
|
||||
/// Pack description
|
||||
#[schema(example = "Integration with Slack for messaging and notifications")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Pack version
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Tags
|
||||
#[schema(example = json!(["messaging", "collaboration"]))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Is standard pack
|
||||
#[schema(example = false)]
|
||||
pub is_standard: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from Pack model to PackResponse
|
||||
impl From<attune_common::models::Pack> for PackResponse {
|
||||
fn from(pack: attune_common::models::Pack) -> Self {
|
||||
Self {
|
||||
id: pack.id,
|
||||
r#ref: pack.r#ref,
|
||||
label: pack.label,
|
||||
description: pack.description,
|
||||
version: pack.version,
|
||||
conf_schema: pack.conf_schema,
|
||||
config: pack.config,
|
||||
meta: pack.meta,
|
||||
tags: pack.tags,
|
||||
runtime_deps: pack.runtime_deps,
|
||||
is_standard: pack.is_standard,
|
||||
created: pack.created,
|
||||
updated: pack.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Pack model to PackSummary
|
||||
impl From<attune_common::models::Pack> for PackSummary {
|
||||
fn from(pack: attune_common::models::Pack) -> Self {
|
||||
Self {
|
||||
id: pack.id,
|
||||
r#ref: pack.r#ref,
|
||||
label: pack.label,
|
||||
description: pack.description,
|
||||
version: pack.version,
|
||||
tags: pack.tags,
|
||||
is_standard: pack.is_standard,
|
||||
created: pack.created,
|
||||
updated: pack.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for pack workflow sync operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackWorkflowSyncResponse {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Number of workflows loaded from filesystem
|
||||
pub loaded_count: usize,
|
||||
/// Number of workflows registered/updated in database
|
||||
pub registered_count: usize,
|
||||
/// Individual workflow registration results
|
||||
pub workflows: Vec<WorkflowSyncResult>,
|
||||
/// Any errors encountered during sync
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// Individual workflow sync result
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct WorkflowSyncResult {
|
||||
/// Workflow reference name
|
||||
pub ref_name: String,
|
||||
/// Whether the workflow was created (false = updated)
|
||||
pub created: bool,
|
||||
/// Workflow definition ID
|
||||
pub workflow_def_id: i64,
|
||||
/// Any warnings during registration
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Response for pack workflow validation operation
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct PackWorkflowValidationResponse {
|
||||
/// Pack reference
|
||||
pub pack_ref: String,
|
||||
/// Number of workflows validated
|
||||
pub validated_count: usize,
|
||||
/// Number of workflows with errors
|
||||
pub error_count: usize,
|
||||
/// Validation errors by workflow reference
|
||||
pub errors: std::collections::HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
fn default_empty_object() -> JsonValue {
|
||||
serde_json::json!({})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_pack_request_defaults() {
|
||||
let json = r#"{
|
||||
"ref": "test-pack",
|
||||
"label": "Test Pack",
|
||||
"version": "1.0.0"
|
||||
}"#;
|
||||
|
||||
let req: CreatePackRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(req.r#ref, "test-pack");
|
||||
assert_eq!(req.label, "Test Pack");
|
||||
assert_eq!(req.version, "1.0.0");
|
||||
assert!(req.tags.is_empty());
|
||||
assert!(req.runtime_deps.is_empty());
|
||||
assert!(!req.is_standard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_pack_request_validation() {
|
||||
let req = CreatePackRequest {
|
||||
r#ref: "".to_string(), // Invalid: empty
|
||||
label: "Test".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: None,
|
||||
conf_schema: default_empty_object(),
|
||||
config: default_empty_object(),
|
||||
meta: default_empty_object(),
|
||||
tags: vec![],
|
||||
runtime_deps: vec![],
|
||||
is_standard: false,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
}
|
||||
363
crates/api/src/dto/rule.rs
Normal file
363
crates/api/src/dto/rule.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
//! Rule DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for creating a new rule
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateRuleRequest {
|
||||
/// Unique reference identifier (e.g., "mypack.notify_on_error")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack.notify_on_error")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference this rule belongs to
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Notify on Error")]
|
||||
pub label: String,
|
||||
|
||||
/// Rule description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Send Slack notification when an error occurs")]
|
||||
pub description: String,
|
||||
|
||||
/// Action reference to execute when rule matches
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Trigger reference that activates this rule
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "system.error_event")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Conditions for rule evaluation (JSON Logic or custom format)
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"var": "event.severity", ">=": 3}))]
|
||||
pub conditions: JsonValue,
|
||||
|
||||
/// Parameters to pass to the action when rule is triggered
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"message": "hello, world"}))]
|
||||
pub action_params: JsonValue,
|
||||
|
||||
/// Parameters for trigger configuration and event filtering
|
||||
#[serde(default = "default_empty_object")]
|
||||
#[schema(value_type = Object, example = json!({"severity": "high"}))]
|
||||
pub trigger_params: JsonValue,
|
||||
|
||||
/// Whether the rule is enabled
|
||||
#[serde(default = "default_true")]
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for updating a rule
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateRuleRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Notify on Error (Updated)")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Rule description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Enhanced error notification with filtering")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Conditions for rule evaluation
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub conditions: Option<JsonValue>,
|
||||
|
||||
/// Parameters to pass to the action when rule is triggered
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub action_params: Option<JsonValue>,
|
||||
|
||||
/// Parameters for trigger configuration and event filtering
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub trigger_params: Option<JsonValue>,
|
||||
|
||||
/// Whether the rule is enabled
|
||||
#[schema(example = false)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response DTO for rule information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RuleResponse {
|
||||
/// Rule ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.notify_on_error")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack ID
|
||||
#[schema(example = 1)]
|
||||
pub pack: i64,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Notify on Error")]
|
||||
pub label: String,
|
||||
|
||||
/// Rule description
|
||||
#[schema(example = "Send Slack notification when an error occurs")]
|
||||
pub description: String,
|
||||
|
||||
/// Action ID
|
||||
#[schema(example = 1)]
|
||||
pub action: i64,
|
||||
|
||||
/// Action reference
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub trigger: i64,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "system.error_event")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Conditions for rule evaluation
|
||||
#[schema(value_type = Object)]
|
||||
pub conditions: JsonValue,
|
||||
|
||||
/// Parameters to pass to the action when rule is triggered
|
||||
#[schema(value_type = Object)]
|
||||
pub action_params: JsonValue,
|
||||
|
||||
/// Parameters for trigger configuration and event filtering
|
||||
#[schema(value_type = Object)]
|
||||
pub trigger_params: JsonValue,
|
||||
|
||||
/// Whether the rule is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Whether this is an ad-hoc rule (not from pack installation)
|
||||
#[schema(example = false)]
|
||||
pub is_adhoc: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified rule response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RuleSummary {
|
||||
/// Rule ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.notify_on_error")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Notify on Error")]
|
||||
pub label: String,
|
||||
|
||||
/// Rule description
|
||||
#[schema(example = "Send Slack notification when an error occurs")]
|
||||
pub description: String,
|
||||
|
||||
/// Action reference
|
||||
#[schema(example = "slack.post_message")]
|
||||
pub action_ref: String,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "system.error_event")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Parameters to pass to the action when rule is triggered
|
||||
#[schema(value_type = Object)]
|
||||
pub action_params: JsonValue,
|
||||
|
||||
/// Parameters for trigger configuration and event filtering
|
||||
#[schema(value_type = Object)]
|
||||
pub trigger_params: JsonValue,
|
||||
|
||||
/// Whether the rule is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from Rule model to RuleResponse
|
||||
impl From<attune_common::models::rule::Rule> for RuleResponse {
|
||||
fn from(rule: attune_common::models::rule::Rule) -> Self {
|
||||
Self {
|
||||
id: rule.id,
|
||||
r#ref: rule.r#ref,
|
||||
pack: rule.pack,
|
||||
pack_ref: rule.pack_ref,
|
||||
label: rule.label,
|
||||
description: rule.description,
|
||||
action: rule.action,
|
||||
action_ref: rule.action_ref,
|
||||
trigger: rule.trigger,
|
||||
trigger_ref: rule.trigger_ref,
|
||||
conditions: rule.conditions,
|
||||
action_params: rule.action_params,
|
||||
trigger_params: rule.trigger_params,
|
||||
enabled: rule.enabled,
|
||||
is_adhoc: rule.is_adhoc,
|
||||
created: rule.created,
|
||||
updated: rule.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Rule model to RuleSummary
|
||||
impl From<attune_common::models::rule::Rule> for RuleSummary {
|
||||
fn from(rule: attune_common::models::rule::Rule) -> Self {
|
||||
Self {
|
||||
id: rule.id,
|
||||
r#ref: rule.r#ref,
|
||||
pack_ref: rule.pack_ref,
|
||||
label: rule.label,
|
||||
description: rule.description,
|
||||
action_ref: rule.action_ref,
|
||||
trigger_ref: rule.trigger_ref,
|
||||
action_params: rule.action_params,
|
||||
trigger_params: rule.trigger_params,
|
||||
enabled: rule.enabled,
|
||||
created: rule.created,
|
||||
updated: rule.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_empty_object() -> JsonValue {
|
||||
serde_json::json!({})
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_rule_request_defaults() {
|
||||
let json = r#"{
|
||||
"ref": "test-rule",
|
||||
"pack_ref": "test-pack",
|
||||
"label": "Test Rule",
|
||||
"description": "Test description",
|
||||
"action_ref": "test.action",
|
||||
"trigger_ref": "test.trigger"
|
||||
}"#;
|
||||
|
||||
let req: CreateRuleRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(req.r#ref, "test-rule");
|
||||
assert_eq!(req.label, "Test Rule");
|
||||
assert_eq!(req.action_ref, "test.action");
|
||||
assert_eq!(req.trigger_ref, "test.trigger");
|
||||
assert!(req.enabled);
|
||||
assert_eq!(req.conditions, serde_json::json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_rule_request_validation() {
|
||||
let req = CreateRuleRequest {
|
||||
r#ref: "".to_string(), // Invalid: empty
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
action_ref: "test.action".to_string(),
|
||||
trigger_ref: "test.trigger".to_string(),
|
||||
conditions: default_empty_object(),
|
||||
action_params: default_empty_object(),
|
||||
trigger_params: default_empty_object(),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_rule_request_valid() {
|
||||
let req = CreateRuleRequest {
|
||||
r#ref: "test.rule".to_string(),
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Rule".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
action_ref: "test.action".to_string(),
|
||||
trigger_ref: "test.trigger".to_string(),
|
||||
conditions: serde_json::json!({
|
||||
"and": [
|
||||
{"var": "event.status", "==": "error"},
|
||||
{"var": "event.severity", ">": 3}
|
||||
]
|
||||
}),
|
||||
action_params: default_empty_object(),
|
||||
trigger_params: default_empty_object(),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_rule_request_all_none() {
|
||||
let req = UpdateRuleRequest {
|
||||
label: None,
|
||||
description: None,
|
||||
conditions: None,
|
||||
action_params: None,
|
||||
trigger_params: None,
|
||||
enabled: None,
|
||||
};
|
||||
|
||||
// Should be valid even with all None values
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_rule_request_partial() {
|
||||
let req = UpdateRuleRequest {
|
||||
label: Some("Updated Rule".to_string()),
|
||||
description: None,
|
||||
conditions: Some(serde_json::json!({"var": "status", "==": "ok"})),
|
||||
action_params: None,
|
||||
trigger_params: None,
|
||||
enabled: Some(false),
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
519
crates/api/src/dto/trigger.rs
Normal file
519
crates/api/src/dto/trigger.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
//! Trigger and Sensor DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for creating a new trigger
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateTriggerRequest {
|
||||
/// Unique reference identifier (e.g., "core.webhook", "system.timer")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "core.webhook")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Optional pack reference this trigger belongs to
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "core")]
|
||||
pub pack_ref: Option<String>,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Webhook Trigger")]
|
||||
pub label: String,
|
||||
|
||||
/// Trigger description
|
||||
#[schema(example = "Triggers when a webhook is received")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Parameter schema (JSON Schema) defining event payload structure
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"type": "object", "properties": {"url": {"type": "string"}}}))]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema (JSON Schema) defining event data structure
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"type": "object", "properties": {"payload": {"type": "object"}}}))]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Whether the trigger is enabled
|
||||
#[serde(default = "default_true")]
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for updating a trigger
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateTriggerRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Webhook Trigger (Updated)")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Trigger description
|
||||
#[schema(example = "Updated webhook trigger description")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Whether the trigger is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response DTO for trigger information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct TriggerResponse {
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "core.webhook")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack ID (optional)
|
||||
#[schema(example = 1)]
|
||||
pub pack: Option<i64>,
|
||||
|
||||
/// Pack reference (optional)
|
||||
#[schema(example = "core")]
|
||||
pub pack_ref: Option<String>,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Webhook Trigger")]
|
||||
pub label: String,
|
||||
|
||||
/// Trigger description
|
||||
#[schema(example = "Triggers when a webhook is received")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Whether the trigger is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Whether webhooks are enabled for this trigger
|
||||
#[schema(example = false)]
|
||||
pub webhook_enabled: bool,
|
||||
|
||||
/// Webhook key (only present if webhooks are enabled)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(example = "wh_k7j2n9p4m8q1r5w3x6z0a2b5c8d1e4f7g9h2")]
|
||||
pub webhook_key: Option<String>,
|
||||
|
||||
/// Whether this is an ad-hoc trigger (not from pack installation)
|
||||
#[schema(example = false)]
|
||||
pub is_adhoc: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified trigger response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct TriggerSummary {
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "core.webhook")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference (optional)
|
||||
#[schema(example = "core")]
|
||||
pub pack_ref: Option<String>,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Webhook Trigger")]
|
||||
pub label: String,
|
||||
|
||||
/// Trigger description
|
||||
#[schema(example = "Triggers when a webhook is received")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Whether the trigger is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Whether webhooks are enabled for this trigger
|
||||
#[schema(example = false)]
|
||||
pub webhook_enabled: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Request DTO for creating a new sensor
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateSensorRequest {
|
||||
/// Unique reference identifier (e.g., "mypack.cpu_monitor")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "monitoring.cpu_sensor")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference this sensor belongs to
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "monitoring")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "CPU Monitoring Sensor")]
|
||||
pub label: String,
|
||||
|
||||
/// Sensor description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Monitors CPU usage and generates events")]
|
||||
pub description: String,
|
||||
|
||||
/// Entry point for sensor execution (e.g., path to script, function name)
|
||||
#[validate(length(min = 1, max = 1024))]
|
||||
#[schema(example = "/sensors/monitoring/cpu_monitor.py")]
|
||||
pub entrypoint: String,
|
||||
|
||||
/// Runtime reference for this sensor
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "python3")]
|
||||
pub runtime_ref: String,
|
||||
|
||||
/// Trigger reference this sensor monitors for
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "monitoring.cpu_threshold")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Parameter schema (JSON Schema) for sensor configuration
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"type": "object", "properties": {"threshold": {"type": "number"}}}))]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Configuration values for this sensor instance (conforms to param_schema)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object, nullable = true, example = json!({"interval": 60, "threshold": 80}))]
|
||||
pub config: Option<JsonValue>,
|
||||
|
||||
/// Whether the sensor is enabled
|
||||
#[serde(default = "default_true")]
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for updating a sensor
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateSensorRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "CPU Monitoring Sensor (Updated)")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Sensor description
|
||||
#[validate(length(min = 1))]
|
||||
#[schema(example = "Enhanced CPU monitoring with alerts")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Entry point for sensor execution
|
||||
#[validate(length(min = 1, max = 1024))]
|
||||
#[schema(example = "/sensors/monitoring/cpu_monitor_v2.py")]
|
||||
pub entrypoint: Option<String>,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Whether the sensor is enabled
|
||||
#[schema(example = false)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response DTO for sensor information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct SensorResponse {
|
||||
/// Sensor ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "monitoring.cpu_sensor")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack ID (optional)
|
||||
#[schema(example = 1)]
|
||||
pub pack: Option<i64>,
|
||||
|
||||
/// Pack reference (optional)
|
||||
#[schema(example = "monitoring")]
|
||||
pub pack_ref: Option<String>,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "CPU Monitoring Sensor")]
|
||||
pub label: String,
|
||||
|
||||
/// Sensor description
|
||||
#[schema(example = "Monitors CPU usage and generates events")]
|
||||
pub description: String,
|
||||
|
||||
/// Entry point
|
||||
#[schema(example = "/sensors/monitoring/cpu_monitor.py")]
|
||||
pub entrypoint: String,
|
||||
|
||||
/// Runtime ID
|
||||
#[schema(example = 1)]
|
||||
pub runtime: i64,
|
||||
|
||||
/// Runtime reference
|
||||
#[schema(example = "python3")]
|
||||
pub runtime_ref: String,
|
||||
|
||||
/// Trigger ID
|
||||
#[schema(example = 1)]
|
||||
pub trigger: i64,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "monitoring.cpu_threshold")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Whether the sensor is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified sensor response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct SensorSummary {
|
||||
/// Sensor ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "monitoring.cpu_sensor")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference (optional)
|
||||
#[schema(example = "monitoring")]
|
||||
pub pack_ref: Option<String>,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "CPU Monitoring Sensor")]
|
||||
pub label: String,
|
||||
|
||||
/// Sensor description
|
||||
#[schema(example = "Monitors CPU usage and generates events")]
|
||||
pub description: String,
|
||||
|
||||
/// Trigger reference
|
||||
#[schema(example = "monitoring.cpu_threshold")]
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Whether the sensor is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from Trigger model to TriggerResponse
|
||||
impl From<attune_common::models::trigger::Trigger> for TriggerResponse {
|
||||
fn from(trigger: attune_common::models::trigger::Trigger) -> Self {
|
||||
Self {
|
||||
id: trigger.id,
|
||||
r#ref: trigger.r#ref,
|
||||
pack: trigger.pack,
|
||||
pack_ref: trigger.pack_ref,
|
||||
label: trigger.label,
|
||||
description: trigger.description,
|
||||
enabled: trigger.enabled,
|
||||
param_schema: trigger.param_schema,
|
||||
out_schema: trigger.out_schema,
|
||||
webhook_enabled: trigger.webhook_enabled,
|
||||
webhook_key: trigger.webhook_key,
|
||||
is_adhoc: trigger.is_adhoc,
|
||||
created: trigger.created,
|
||||
updated: trigger.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Trigger model to TriggerSummary
|
||||
impl From<attune_common::models::trigger::Trigger> for TriggerSummary {
|
||||
fn from(trigger: attune_common::models::trigger::Trigger) -> Self {
|
||||
Self {
|
||||
id: trigger.id,
|
||||
r#ref: trigger.r#ref,
|
||||
pack_ref: trigger.pack_ref,
|
||||
label: trigger.label,
|
||||
description: trigger.description,
|
||||
enabled: trigger.enabled,
|
||||
webhook_enabled: trigger.webhook_enabled,
|
||||
created: trigger.created,
|
||||
updated: trigger.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Sensor model to SensorResponse
|
||||
impl From<attune_common::models::trigger::Sensor> for SensorResponse {
|
||||
fn from(sensor: attune_common::models::trigger::Sensor) -> Self {
|
||||
Self {
|
||||
id: sensor.id,
|
||||
r#ref: sensor.r#ref,
|
||||
pack: sensor.pack,
|
||||
pack_ref: sensor.pack_ref,
|
||||
label: sensor.label,
|
||||
description: sensor.description,
|
||||
entrypoint: sensor.entrypoint,
|
||||
runtime: sensor.runtime,
|
||||
runtime_ref: sensor.runtime_ref,
|
||||
trigger: sensor.trigger,
|
||||
trigger_ref: sensor.trigger_ref,
|
||||
enabled: sensor.enabled,
|
||||
param_schema: sensor.param_schema,
|
||||
created: sensor.created,
|
||||
updated: sensor.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from Sensor model to SensorSummary
|
||||
impl From<attune_common::models::trigger::Sensor> for SensorSummary {
|
||||
fn from(sensor: attune_common::models::trigger::Sensor) -> Self {
|
||||
Self {
|
||||
id: sensor.id,
|
||||
r#ref: sensor.r#ref,
|
||||
pack_ref: sensor.pack_ref,
|
||||
label: sensor.label,
|
||||
description: sensor.description,
|
||||
trigger_ref: sensor.trigger_ref,
|
||||
enabled: sensor.enabled,
|
||||
created: sensor.created,
|
||||
updated: sensor.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_trigger_request_defaults() {
|
||||
let json = r#"{
|
||||
"ref": "test-trigger",
|
||||
"label": "Test Trigger"
|
||||
}"#;
|
||||
|
||||
let req: CreateTriggerRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(req.r#ref, "test-trigger");
|
||||
assert_eq!(req.label, "Test Trigger");
|
||||
assert!(req.enabled);
|
||||
assert!(req.pack_ref.is_none());
|
||||
assert!(req.description.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_trigger_request_validation() {
|
||||
let req = CreateTriggerRequest {
|
||||
r#ref: "".to_string(), // Invalid: empty
|
||||
pack_ref: None,
|
||||
label: "Test Trigger".to_string(),
|
||||
description: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_sensor_request_valid() {
|
||||
let req = CreateSensorRequest {
|
||||
r#ref: "test.sensor".to_string(),
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Sensor".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
entrypoint: "/sensors/test.py".to_string(),
|
||||
runtime_ref: "python3".to_string(),
|
||||
trigger_ref: "test.trigger".to_string(),
|
||||
param_schema: None,
|
||||
config: None,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_trigger_request_all_none() {
|
||||
let req = UpdateTriggerRequest {
|
||||
label: None,
|
||||
description: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
enabled: None,
|
||||
};
|
||||
|
||||
// Should be valid even with all None values
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_sensor_request_partial() {
|
||||
let req = UpdateSensorRequest {
|
||||
label: Some("Updated Sensor".to_string()),
|
||||
description: None,
|
||||
entrypoint: Some("/sensors/test_v2.py".to_string()),
|
||||
param_schema: None,
|
||||
enabled: Some(false),
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
41
crates/api/src/dto/webhook.rs
Normal file
41
crates/api/src/dto/webhook.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Webhook-related DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Request body for webhook receiver endpoint
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct WebhookReceiverRequest {
|
||||
/// Webhook payload (arbitrary JSON)
|
||||
pub payload: JsonValue,
|
||||
|
||||
/// Optional headers from the webhook request (for logging/debugging)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub headers: Option<JsonValue>,
|
||||
|
||||
/// Optional source IP address
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_ip: Option<String>,
|
||||
|
||||
/// Optional user agent
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from webhook receiver endpoint
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct WebhookReceiverResponse {
|
||||
/// ID of the event created from this webhook
|
||||
pub event_id: i64,
|
||||
|
||||
/// Reference of the trigger that received this webhook
|
||||
pub trigger_ref: String,
|
||||
|
||||
/// Timestamp when the webhook was received
|
||||
pub received_at: DateTime<Utc>,
|
||||
|
||||
/// Success message
|
||||
pub message: String,
|
||||
}
|
||||
327
crates/api/src/dto/workflow.rs
Normal file
327
crates/api/src/dto/workflow.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
//! Workflow DTOs for API requests and responses
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use validator::Validate;
|
||||
|
||||
/// Request DTO for creating a new workflow
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateWorkflowRequest {
|
||||
/// Unique reference identifier (e.g., "core.notify_on_failure", "slack.incident_workflow")
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack.incident_workflow")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference this workflow belongs to
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Incident Response Workflow")]
|
||||
pub label: String,
|
||||
|
||||
/// Workflow description
|
||||
#[schema(example = "Automated incident response workflow with notifications and approvals")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Workflow version (semantic versioning recommended)
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Parameter schema (JSON Schema) defining expected inputs
|
||||
#[schema(value_type = Object, example = json!({"type": "object", "properties": {"severity": {"type": "string"}, "channel": {"type": "string"}}}))]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema (JSON Schema) defining expected outputs
|
||||
#[schema(value_type = Object, example = json!({"type": "object", "properties": {"incident_id": {"type": "string"}}}))]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Workflow definition (complete workflow YAML structure as JSON)
|
||||
#[schema(value_type = Object)]
|
||||
pub definition: JsonValue,
|
||||
|
||||
/// Tags for categorization and search
|
||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||
pub tags: Option<Vec<String>>,
|
||||
|
||||
/// Whether the workflow is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request DTO for updating a workflow
|
||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateWorkflowRequest {
|
||||
/// Human-readable label
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
#[schema(example = "Incident Response Workflow (Updated)")]
|
||||
pub label: Option<String>,
|
||||
|
||||
/// Workflow description
|
||||
#[schema(example = "Enhanced incident response workflow with additional automation")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Workflow version
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
#[schema(example = "1.1.0")]
|
||||
pub version: Option<String>,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Workflow definition
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub definition: Option<JsonValue>,
|
||||
|
||||
/// Tags
|
||||
#[schema(example = json!(["incident", "slack", "approval", "automation"]))]
|
||||
pub tags: Option<Vec<String>>,
|
||||
|
||||
/// Whether the workflow is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response DTO for workflow information
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct WorkflowResponse {
|
||||
/// Workflow ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.incident_workflow")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack ID
|
||||
#[schema(example = 1)]
|
||||
pub pack: i64,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Incident Response Workflow")]
|
||||
pub label: String,
|
||||
|
||||
/// Workflow description
|
||||
#[schema(example = "Automated incident response workflow with notifications and approvals")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Workflow version
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Parameter schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub param_schema: Option<JsonValue>,
|
||||
|
||||
/// Output schema
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Workflow definition
|
||||
#[schema(value_type = Object)]
|
||||
pub definition: JsonValue,
|
||||
|
||||
/// Tags
|
||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Whether the workflow is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Simplified workflow response (for list endpoints)
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct WorkflowSummary {
|
||||
/// Workflow ID
|
||||
#[schema(example = 1)]
|
||||
pub id: i64,
|
||||
|
||||
/// Unique reference identifier
|
||||
#[schema(example = "slack.incident_workflow")]
|
||||
pub r#ref: String,
|
||||
|
||||
/// Pack reference
|
||||
#[schema(example = "slack")]
|
||||
pub pack_ref: String,
|
||||
|
||||
/// Human-readable label
|
||||
#[schema(example = "Incident Response Workflow")]
|
||||
pub label: String,
|
||||
|
||||
/// Workflow description
|
||||
#[schema(example = "Automated incident response workflow with notifications and approvals")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Workflow version
|
||||
#[schema(example = "1.0.0")]
|
||||
pub version: String,
|
||||
|
||||
/// Tags
|
||||
#[schema(example = json!(["incident", "slack", "approval"]))]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Whether the workflow is enabled
|
||||
#[schema(example = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Convert from WorkflowDefinition model to WorkflowResponse
|
||||
impl From<attune_common::models::workflow::WorkflowDefinition> for WorkflowResponse {
|
||||
fn from(workflow: attune_common::models::workflow::WorkflowDefinition) -> Self {
|
||||
Self {
|
||||
id: workflow.id,
|
||||
r#ref: workflow.r#ref,
|
||||
pack: workflow.pack,
|
||||
pack_ref: workflow.pack_ref,
|
||||
label: workflow.label,
|
||||
description: workflow.description,
|
||||
version: workflow.version,
|
||||
param_schema: workflow.param_schema,
|
||||
out_schema: workflow.out_schema,
|
||||
definition: workflow.definition,
|
||||
tags: workflow.tags,
|
||||
enabled: workflow.enabled,
|
||||
created: workflow.created,
|
||||
updated: workflow.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from WorkflowDefinition model to WorkflowSummary
|
||||
impl From<attune_common::models::workflow::WorkflowDefinition> for WorkflowSummary {
|
||||
fn from(workflow: attune_common::models::workflow::WorkflowDefinition) -> Self {
|
||||
Self {
|
||||
id: workflow.id,
|
||||
r#ref: workflow.r#ref,
|
||||
pack_ref: workflow.pack_ref,
|
||||
label: workflow.label,
|
||||
description: workflow.description,
|
||||
version: workflow.version,
|
||||
tags: workflow.tags,
|
||||
enabled: workflow.enabled,
|
||||
created: workflow.created,
|
||||
updated: workflow.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Query parameters for workflow search and filtering
|
||||
#[derive(Debug, Clone, Deserialize, Validate, IntoParams)]
|
||||
pub struct WorkflowSearchParams {
|
||||
/// Filter by tag(s) - comma-separated list
|
||||
#[param(example = "incident,approval")]
|
||||
pub tags: Option<String>,
|
||||
|
||||
/// Filter by enabled status
|
||||
#[param(example = true)]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// Search term for label/description (case-insensitive)
|
||||
#[param(example = "incident")]
|
||||
pub search: Option<String>,
|
||||
|
||||
/// Filter by pack reference
|
||||
#[param(example = "core")]
|
||||
pub pack_ref: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_workflow_request_validation() {
|
||||
let req = CreateWorkflowRequest {
|
||||
r#ref: "".to_string(), // Invalid: empty
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Workflow".to_string(),
|
||||
description: Some("Test description".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: serde_json::json!({"tasks": []}),
|
||||
tags: None,
|
||||
enabled: None,
|
||||
};
|
||||
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_workflow_request_valid() {
|
||||
let req = CreateWorkflowRequest {
|
||||
r#ref: "test.workflow".to_string(),
|
||||
pack_ref: "test-pack".to_string(),
|
||||
label: "Test Workflow".to_string(),
|
||||
description: Some("Test description".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: serde_json::json!({"tasks": []}),
|
||||
tags: Some(vec!["test".to_string()]),
|
||||
enabled: Some(true),
|
||||
};
|
||||
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_workflow_request_all_none() {
|
||||
let req = UpdateWorkflowRequest {
|
||||
label: None,
|
||||
description: None,
|
||||
version: None,
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: None,
|
||||
tags: None,
|
||||
enabled: None,
|
||||
};
|
||||
|
||||
// Should be valid even with all None values
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workflow_search_params() {
|
||||
let params = WorkflowSearchParams {
|
||||
tags: Some("incident,approval".to_string()),
|
||||
enabled: Some(true),
|
||||
search: Some("response".to_string()),
|
||||
pack_ref: Some("core".to_string()),
|
||||
};
|
||||
|
||||
assert!(params.validate().is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user