use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uptrakit_shared_types::PluginTypeId;
use crate::validation::{Validate, ValidationError};
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct PluginTypeSettingsResponse {
pub plugin_type: PluginTypeId,
pub config: serde_json::Value,
#[serde(with = "time::serde::rfc3339")]
#[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
#[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
pub updated_at: OffsetDateTime,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpsertPluginTypeSettingsRequest {
pub config: serde_json::Value,
}
impl Validate for UpsertPluginTypeSettingsRequest {
fn validate(&self) -> Result<(), ValidationError> {
if !self.config.is_object() {
return Err(ValidationError {
field: "config",
message: "config must be a JSON object".to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::assertions_on_result_states,
reason = "test assertions — is_ok/is_err provides readable failure messages"
)]
use super::*;
use uptrakit_shared_types::plugin_ids;
#[test]
fn upsert_request_round_trip() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::json!({"poll_interval_secs": 300}),
};
let json = serde_json::to_string(&req).expect("serialization should succeed");
let de: UpsertPluginTypeSettingsRequest =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(de.config["poll_interval_secs"], 300);
}
#[test]
fn upsert_request_validate_rejects_null() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::Value::Null,
};
let err = req.validate().expect_err("should reject null config");
assert_eq!(err.field, "config");
}
#[test]
fn upsert_request_validate_rejects_array() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::json!([1, 2, 3]),
};
let err = req.validate().expect_err("should reject array config");
assert_eq!(err.field, "config");
}
#[test]
fn upsert_request_validate_rejects_string() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::json!("not an object"),
};
let err = req.validate().expect_err("should reject string config");
assert_eq!(err.field, "config");
}
#[test]
fn upsert_request_validate_accepts_empty_object() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::json!({}),
};
assert!(req.validate().is_ok());
}
#[test]
fn upsert_request_validate_accepts_populated_object() {
let req = UpsertPluginTypeSettingsRequest {
config: serde_json::json!({"key": "value", "nested": {"a": 1}}),
};
assert!(req.validate().is_ok());
}
#[test]
fn response_round_trip() {
use time::macros::datetime;
let resp = PluginTypeSettingsResponse {
plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
config: serde_json::json!({"poll_interval_secs": 300}),
created_at: datetime!(2025-01-01 00:00:00 UTC),
updated_at: datetime!(2025-06-01 00:00:00 UTC),
};
let json = serde_json::to_string(&resp).expect("serialization should succeed");
let de: PluginTypeSettingsResponse =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(de.plugin_type, plugin_ids::RELEASES_GITHUB.clone());
assert_eq!(de.config["poll_interval_secs"], 300);
assert_eq!(de.created_at, datetime!(2025-01-01 00:00:00 UTC));
assert_eq!(de.updated_at, datetime!(2025-06-01 00:00:00 UTC));
}
#[test]
fn response_timestamps_serialize_as_rfc3339() {
use time::macros::datetime;
let resp = PluginTypeSettingsResponse {
plugin_type: plugin_ids::RELEASES_DOCKER.clone(),
config: serde_json::json!({}),
created_at: datetime!(2025-01-01 00:00:00 UTC),
updated_at: datetime!(2025-06-01 12:30:00 UTC),
};
let json = serde_json::to_string(&resp).expect("serialization should succeed");
assert!(
json.contains("2025-01-01T00:00:00Z"),
"created_at should be RFC 3339"
);
assert!(
json.contains("2025-06-01T12:30:00Z"),
"updated_at should be RFC 3339"
);
}
}