use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::AuthClient;
use crate::config::Config;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Rfi {
pub id: String,
pub title: String,
pub number: Option<String>,
pub status: String,
pub priority: Option<String>,
pub question: Option<String>,
pub answer: Option<String>,
pub due_date: Option<String>,
pub assigned_to: Option<String>,
pub assigned_to_name: Option<String>,
pub created_by: Option<String>,
pub created_by_name: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub closed_at: Option<String>,
pub location: Option<String>,
pub discipline: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RfisResponse {
pub results: Vec<Rfi>,
#[allow(dead_code)]
pub pagination: Option<Pagination>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Pagination {
pub limit: i32,
pub offset: i32,
pub total_results: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum RfiStatus {
Draft,
Open,
Answered,
Closed,
Void,
}
impl RfiStatus {
pub fn as_str(&self) -> &'static str {
match self {
RfiStatus::Draft => "draft",
RfiStatus::Open => "open",
RfiStatus::Answered => "answered",
RfiStatus::Closed => "closed",
RfiStatus::Void => "void",
}
}
}
impl std::fmt::Display for RfiStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum RfiPriority {
Low,
Normal,
High,
Critical,
}
impl RfiPriority {
pub fn as_str(&self) -> &'static str {
match self {
RfiPriority::Low => "low",
RfiPriority::Normal => "normal",
RfiPriority::High => "high",
RfiPriority::Critical => "critical",
}
}
}
impl std::fmt::Display for RfiPriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateRfiRequest {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub question: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub due_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discipline: Option<String>,
}
#[derive(Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct UpdateRfiRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub question: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub answer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub due_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assigned_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
}
pub struct RfiClient {
#[allow(dead_code)]
config: Config,
auth: AuthClient,
http_client: reqwest::Client,
}
impl RfiClient {
#[allow(dead_code)]
pub fn new(config: Config, auth: AuthClient) -> Self {
Self::new_with_http_config(config, auth, crate::http::HttpClientConfig::default())
}
pub fn new_with_http_config(
config: Config,
auth: AuthClient,
http_config: crate::http::HttpClientConfig,
) -> Self {
let http_client = http_config
.create_client()
.unwrap_or_else(|_| reqwest::Client::new());
Self {
config,
auth,
http_client,
}
}
pub async fn list_rfis(&self, project_id: &str) -> Result<Vec<Rfi>> {
let token = self.auth.get_3leg_token().await?;
let url = format!(
"https://developer.api.autodesk.com/construction/rfis/v2/projects/{}/rfis",
project_id
);
let response = self
.http_client
.get(&url)
.bearer_auth(&token)
.send()
.await
.context("Failed to list RFIs")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to list RFIs ({}): {}", status, error_text);
}
let rfis_response: RfisResponse = response
.json()
.await
.context("Failed to parse RFIs response")?;
Ok(rfis_response.results)
}
pub async fn get_rfi(&self, project_id: &str, rfi_id: &str) -> Result<Rfi> {
let token = self.auth.get_3leg_token().await?;
let url = format!(
"https://developer.api.autodesk.com/construction/rfis/v2/projects/{}/rfis/{}",
project_id, rfi_id
);
let response = self
.http_client
.get(&url)
.bearer_auth(&token)
.send()
.await
.context("Failed to get RFI")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to get RFI ({}): {}", status, error_text);
}
let rfi: Rfi = response
.json()
.await
.context("Failed to parse RFI response")?;
Ok(rfi)
}
pub async fn create_rfi(&self, project_id: &str, request: CreateRfiRequest) -> Result<Rfi> {
let token = self.auth.get_3leg_token().await?;
let url = format!(
"https://developer.api.autodesk.com/construction/rfis/v2/projects/{}/rfis",
project_id
);
let response = self
.http_client
.post(&url)
.bearer_auth(&token)
.json(&request)
.send()
.await
.context("Failed to create RFI")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to create RFI ({}): {}", status, error_text);
}
let rfi: Rfi = response
.json()
.await
.context("Failed to parse RFI response")?;
Ok(rfi)
}
pub async fn update_rfi(
&self,
project_id: &str,
rfi_id: &str,
request: UpdateRfiRequest,
) -> Result<Rfi> {
let token = self.auth.get_3leg_token().await?;
let url = format!(
"https://developer.api.autodesk.com/construction/rfis/v2/projects/{}/rfis/{}",
project_id, rfi_id
);
let response = self
.http_client
.patch(&url)
.bearer_auth(&token)
.json(&request)
.send()
.await
.context("Failed to update RFI")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to update RFI ({}): {}", status, error_text);
}
let rfi: Rfi = response
.json()
.await
.context("Failed to parse RFI response")?;
Ok(rfi)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rfi_status_display() {
assert_eq!(RfiStatus::Open.as_str(), "open");
assert_eq!(RfiStatus::Closed.as_str(), "closed");
assert_eq!(RfiStatus::Answered.as_str(), "answered");
}
#[test]
fn test_rfi_priority_display() {
assert_eq!(RfiPriority::High.as_str(), "high");
assert_eq!(RfiPriority::Critical.as_str(), "critical");
}
#[test]
fn test_create_rfi_request_serialization() {
let request = CreateRfiRequest {
title: "Test RFI".to_string(),
question: Some("What is the answer?".to_string()),
priority: Some("high".to_string()),
due_date: None,
assigned_to: None,
location: None,
discipline: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("Test RFI"));
assert!(json.contains("high"));
assert!(!json.contains("dueDate"));
}
#[test]
fn test_update_rfi_request_serialization() {
let request = UpdateRfiRequest {
status: Some("closed".to_string()),
answer: Some("The answer is 42".to_string()),
..Default::default()
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("closed"));
assert!(json.contains("42"));
assert!(!json.contains("title"));
}
}