use serde::{Deserialize, Serialize};
use uptrakit_wire::surfaces::{self, SurfaceDescriptor};
use uuid::Uuid;
use crate::validation::{Validate, ValidationError};
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
pub struct ListSurfacesQuery {
#[serde(default)]
pub slot: Option<String>,
#[serde(default)]
pub page: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SurfaceResponse {
#[serde(flatten)]
#[cfg_attr(feature = "openapi", schema(value_type = serde_json::Value))]
pub descriptor: SurfaceDescriptor,
pub provider_count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum SurfaceProviderAvailability {
Available,
Disconnected,
IncompatibleTenant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SurfaceProviderInfo {
pub provider_id: String,
pub display_label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_id: Option<Uuid>,
pub availability: SurfaceProviderAvailability,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
pub encryption_metadata: Option<surfaces::ProviderEncryptionMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SurfaceReadResponse {
#[cfg_attr(feature = "openapi", schema(value_type = serde_json::Value))]
pub descriptor: SurfaceDescriptor,
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(value_type = Vec<serde_json::Value>))]
pub interactions: Vec<surfaces::InteractionDescriptor>,
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(value_type = Vec<serde_json::Value>))]
pub data_sources: Vec<surfaces::DataSourceDescriptor>,
}
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(
feature = "openapi",
derive(utoipa::IntoParams),
into_params(parameter_in = Query)
)]
pub struct ReadSurfaceInteractionQuery {
#[serde(default)]
pub target_provider_id: Option<String>,
#[serde(default)]
pub timeout_seconds: Option<u16>,
#[serde(default)]
pub page: Option<u64>,
#[serde(default)]
pub per_page: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct InvokeSurfaceInteractionRequest {
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(value_type = serde_json::Value))]
pub params: serde_json::Map<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
pub encrypted_sensitive_params: Option<surfaces::EncryptedSensitiveParams>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_provider_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_seconds: Option<u16>,
}
impl Validate for InvokeSurfaceInteractionRequest {
fn validate(&self) -> Result<(), ValidationError> {
Ok(())
}
}
impl crate::validation::sealed::Sealed for InvokeSurfaceInteractionRequest {}
impl crate::validation::RoutingEnvelope for InvokeSurfaceInteractionRequest {
fn routing_envelope(&self) -> crate::validation::InvokeRoutingEnvelope {
crate::validation::InvokeRoutingEnvelope {
target_provider_id: self.target_provider_id.clone(),
timeout_seconds: self.timeout_seconds,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use uptrakit_wire::limits::MAX_SURFACE_PARAMS_LEN;
#[test]
fn invoke_surface_interaction_request_validate_is_ok() {
InvokeSurfaceInteractionRequest::default()
.validate()
.expect("InvokeSurfaceInteractionRequest::default() should validate");
}
#[test]
fn invoke_request_validate_is_unconditionally_ok_canary() {
let base = InvokeSurfaceInteractionRequest {
params: serde_json::Map::from_iter([(
"k".to_string(),
serde_json::Value::String("v".to_string()),
)]),
encrypted_sensitive_params: None,
target_provider_id: Some("provider".to_string()),
idempotency_key: Some("key".to_string()),
timeout_seconds: Some(1),
};
let hostile_fixtures = [
InvokeSurfaceInteractionRequest {
target_provider_id: Some(String::new()),
..base.clone()
},
InvokeSurfaceInteractionRequest {
target_provider_id: Some(" ".to_string()),
..base.clone()
},
InvokeSurfaceInteractionRequest {
idempotency_key: Some(String::new()),
..base.clone()
},
InvokeSurfaceInteractionRequest {
idempotency_key: Some(" ".to_string()),
..base.clone()
},
InvokeSurfaceInteractionRequest {
timeout_seconds: Some(0),
..base.clone()
},
InvokeSurfaceInteractionRequest {
params: serde_json::Map::from_iter([(
String::new(),
serde_json::Value::String("x".repeat(MAX_SURFACE_PARAMS_LEN + 1)),
)]),
..base.clone()
},
];
for fixture in hostile_fixtures {
fixture.validate().expect(
"validate() must stay unconditionally Ok until the discriminating test exists",
);
}
}
#[test]
fn routing_envelope_projects_only_the_envelope_fields() {
let req = InvokeSurfaceInteractionRequest {
target_provider_id: Some("p1".to_string()),
timeout_seconds: Some(30),
..Default::default()
};
let env = crate::validation::RoutingEnvelope::routing_envelope(&req);
assert_eq!(env.target_provider_id.as_deref(), Some("p1"));
assert_eq!(env.timeout_seconds, Some(30));
}
}