re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View 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
View 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>,
}

View 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(), &params, 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
View 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
}
}

View 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
}
}

View 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
View 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
View 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
View 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
View 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());
}
}

View 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());
}
}

View 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,
}

View 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());
}
}