//! Minimal Windmill API client for tests //! //! This is a handwritten minimal client that provides just enough functionality //! for the integration tests. It replaces the auto-generated progenitor client. use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Client for Windmill API #[derive(Clone)] pub struct Client { pub baseurl: String, pub client: reqwest::Client, } impl Client { /// Create a new client with an existing reqwest::Client pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { Self { baseurl: baseurl.to_string(), client, } } /// Get the base URL pub fn baseurl(&self) -> &String { &self.baseurl } /// Get the internal reqwest::Client pub fn client(&self) -> &reqwest::Client { &self.client } /// Create a script pub async fn create_script( &self, workspace: &str, body: &types::NewScript, ) -> Result { let url = format!( "{}/w/{}/scripts/create", self.baseurl, urlencoding::encode(workspace) ); let response = self.client.post(&url).json(body).send().await?; if response.status().is_success() { Ok(response.text().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } /// Create a flow pub async fn create_flow( &self, workspace: &str, body: &types::CreateFlowBody, ) -> Result { let url = format!( "{}/w/{}/flows/create", self.baseurl, urlencoding::encode(workspace) ); let response = self.client.post(&url).json(body).send().await?; if response.status().is_success() { Ok(response.text().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } /// Get flow by path pub async fn get_flow_by_path( &self, workspace: &str, path: &str, with_starred_info: Option, ) -> Result { let url = format!( "{}/w/{}/flows/get/{}", self.baseurl, urlencoding::encode(workspace), urlencoding::encode(path) ); let mut request = self.client.get(&url); if let Some(starred) = with_starred_info { request = request.query(&[("with_starred_info", starred.to_string())]); } let response = request.send().await?; if response.status().is_success() { Ok(response.json().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } /// Create a schedule pub async fn create_schedule( &self, workspace: &str, body: &types::NewSchedule, ) -> Result { let url = format!( "{}/w/{}/schedules/create", self.baseurl, urlencoding::encode(workspace) ); let response = self.client.post(&url).json(body).send().await?; if response.status().is_success() { Ok(response.text().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } /// Update a schedule pub async fn update_schedule( &self, workspace: &str, path: &str, body: &types::EditSchedule, ) -> Result { let url = format!( "{}/w/{}/schedules/update/{}", self.baseurl, urlencoding::encode(workspace), urlencoding::encode(path) ); let response = self.client.post(&url).json(body).send().await?; if response.status().is_success() { Ok(response.text().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } /// List workspaces pub async fn list_workspaces(&self) -> Result, Error> { let url = format!("{}/workspaces/list", self.baseurl); let response = self.client.get(&url).send().await?; if response.status().is_success() { Ok(response.json().await?) } else { Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) } } } /// Create a client with bearer token authentication pub fn create_client(base_url: &str, token: String) -> Client { let mut val = HeaderValue::from_str(&format!("Bearer {token}")).expect("header creation"); val.set_sensitive(true); let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, val); let client = reqwest::ClientBuilder::new() .default_headers(headers) .build() .expect("client build"); Client::new_with_client(&format!("{}/api", base_url.trim_end_matches('/')), client) } /// Error type for API client #[derive(Debug)] pub enum Error { /// Request error Request(reqwest::Error), /// Unexpected response status UnexpectedResponse(u16, String), } impl From for Error { fn from(err: reqwest::Error) -> Self { Error::Request(err) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::Request(e) => write!(f, "Request error: {}", e), Error::UnexpectedResponse(status, body) => { write!(f, "Unexpected response ({}): {}", status, body) } } } } impl std::error::Error for Error {} /// API types pub mod types { use super::*; /// Script language #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum ScriptLang { #[serde(rename = "python3")] Python3, #[serde(rename = "deno")] Deno, #[serde(rename = "go")] Go, #[serde(rename = "bash")] Bash, #[serde(rename = "powershell")] Powershell, #[serde(rename = "postgresql")] Postgresql, #[serde(rename = "mysql")] Mysql, #[serde(rename = "bigquery")] Bigquery, #[serde(rename = "snowflake")] Snowflake, #[serde(rename = "mssql")] Mssql, #[serde(rename = "oracledb")] Oracledb, #[serde(rename = "graphql")] Graphql, #[serde(rename = "nativets")] Nativets, #[serde(rename = "bun")] Bun, #[serde(rename = "php")] Php, #[serde(rename = "rust")] Rust, #[serde(rename = "ansible")] Ansible, #[serde(rename = "csharp")] Csharp, #[serde(rename = "nu")] Nu, #[serde(rename = "java")] Java, #[serde(rename = "ruby")] Ruby, #[serde(rename = "duckdb")] Duckdb, } impl std::str::FromStr for ScriptLang { type Err = &'static str; fn from_str(value: &str) -> Result { match value { "python3" => Ok(Self::Python3), "deno" => Ok(Self::Deno), "go" => Ok(Self::Go), "bash" => Ok(Self::Bash), "powershell" => Ok(Self::Powershell), "postgresql" => Ok(Self::Postgresql), "mysql" => Ok(Self::Mysql), "bigquery" => Ok(Self::Bigquery), "snowflake" => Ok(Self::Snowflake), "mssql" => Ok(Self::Mssql), "oracledb" => Ok(Self::Oracledb), "graphql" => Ok(Self::Graphql), "nativets" => Ok(Self::Nativets), "bun" => Ok(Self::Bun), "php" => Ok(Self::Php), "rust" => Ok(Self::Rust), "ansible" => Ok(Self::Ansible), "csharp" => Ok(Self::Csharp), "nu" => Ok(Self::Nu), "java" => Ok(Self::Java), "ruby" => Ok(Self::Ruby), "duckdb" => Ok(Self::Duckdb), _ => Err("invalid script language"), } } } /// Raw script language (for flow modules) #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum RawScriptLanguage { #[serde(rename = "python3")] Python3, #[serde(rename = "deno")] Deno, #[serde(rename = "go")] Go, #[serde(rename = "bash")] Bash, #[serde(rename = "powershell")] Powershell, #[serde(rename = "postgresql")] Postgresql, #[serde(rename = "mysql")] Mysql, #[serde(rename = "bigquery")] Bigquery, #[serde(rename = "snowflake")] Snowflake, #[serde(rename = "mssql")] Mssql, #[serde(rename = "oracledb")] Oracledb, #[serde(rename = "graphql")] Graphql, #[serde(rename = "nativets")] Nativets, #[serde(rename = "bun")] Bun, #[serde(rename = "php")] Php, #[serde(rename = "rust")] Rust, #[serde(rename = "ansible")] Ansible, #[serde(rename = "csharp")] Csharp, #[serde(rename = "nu")] Nu, #[serde(rename = "java")] Java, #[serde(rename = "ruby")] Ruby, #[serde(rename = "duckdb")] Duckdb, } /// New script request body #[derive(Clone, Debug, Serialize)] pub struct NewScript { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub assets: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_ttl: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub codebase: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency_key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency_time_window_s: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrent_limit: Option, pub content: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub dedicated_worker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub delete_after_use: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub deployment_message: Option, pub description: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub draft_only: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub envs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub has_preprocessor: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub is_template: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub kind: Option, pub language: ScriptLang, #[serde(default, skip_serializing_if = "Option::is_none")] pub lock: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub no_main_func: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_behalf_of_email: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub parent_hash: Option, pub path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub restart_unless_cancelled: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub schema: HashMap, pub summary: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub tag: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, } /// Script arguments (used in schedules) pub type ScriptArgs = HashMap; /// New schedule request body #[derive(Clone, Debug, Serialize)] pub struct NewSchedule { pub args: ScriptArgs, #[serde(default, skip_serializing_if = "Option::is_none")] pub cron_version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, pub is_flow: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub no_flow_overlap: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_exact: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_extra_args: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_times: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery_extra_args: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery_times: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_success: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_success_extra_args: Option, pub path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub retry: Option, pub schedule: String, pub script_path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub summary: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tag: Option, pub timezone: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, } /// Edit schedule request body #[derive(Clone, Debug, Serialize)] pub struct EditSchedule { pub args: ScriptArgs, #[serde(default, skip_serializing_if = "Option::is_none")] pub cron_version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub no_flow_overlap: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_exact: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_extra_args: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_failure_times: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery_extra_args: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_recovery_times: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_success: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_success_extra_args: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub retry: Option, pub schedule: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub summary: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tag: Option, pub timezone: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, } /// Open flow definition #[derive(Clone, Debug, Deserialize, Serialize)] pub struct OpenFlow { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub schema: HashMap, pub summary: String, pub value: FlowValue, } /// Flow value containing modules #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FlowValue { #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_ttl: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency_key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency_time_window_s: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrent_limit: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub early_return: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub failure_module: Option, pub modules: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub preprocessor_module: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub same_worker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub skip_expr: Option, } /// Flow module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FlowModule { #[serde(default, skip_serializing_if = "Option::is_none")] pub cache_ttl: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub continue_on_error: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub delete_after_use: Option, pub id: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub mock: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub retry: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub skip_if: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sleep: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub stop_after_all_iters_if: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub stop_after_if: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub summary: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub suspend: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, pub value: FlowModuleValue, } /// Input transform #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum InputTransform { Static { #[serde(rename = "type")] type_: String, value: serde_json::Value }, Javascript { #[serde(rename = "type")] type_: String, expr: String }, } /// Flow module value (the actual module content) #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum FlowModuleValue { RawScript(RawScript), Script(ScriptModule), Flow(FlowModule2), ForLoop(ForLoopModule), WhileLoop(WhileLoopModule), BranchOne(BranchOneModule), BranchAll(BranchAllModule), Identity(IdentityModule), Other(serde_json::Value), } /// Raw script module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RawScript { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub assets: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrency_time_window_s: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub concurrent_limit: Option, pub content: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_concurrency_key: Option, pub input_transforms: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub is_trigger: Option, pub language: RawScriptLanguage, #[serde(default, skip_serializing_if = "Option::is_none")] pub lock: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tag: Option, #[serde(rename = "type")] pub type_: String, } impl RawScript { pub fn new(content: String, language: RawScriptLanguage) -> Self { Self { assets: vec![], concurrency_time_window_s: None, concurrent_limit: None, content, custom_concurrency_key: None, input_transforms: HashMap::new(), is_trigger: None, language, lock: None, path: None, tag: None, type_: "rawscript".to_string(), } } } /// Script module reference #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ScriptModule { #[serde(rename = "type")] pub type_: String, pub path: String, pub input_transforms: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub hash: Option, } /// Flow module reference #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FlowModule2 { #[serde(rename = "type")] pub type_: String, pub path: String, pub input_transforms: HashMap, } /// For loop module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ForLoopModule { #[serde(rename = "type")] pub type_: String, pub iterator: InputTransform, pub modules: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub parallel: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub parallelism: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub skip_failures: Option, } /// While loop module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WhileLoopModule { #[serde(rename = "type")] pub type_: String, pub modules: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub skip_failures: Option, } /// Branch one module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BranchOneModule { #[serde(rename = "type")] pub type_: String, pub branches: Vec, pub default: Vec, } /// Branch all module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BranchAllModule { #[serde(rename = "type")] pub type_: String, pub branches: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub parallel: Option, } /// Identity module #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdentityModule { #[serde(rename = "type")] pub type_: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub flow: Option, } /// Open flow with path #[derive(Clone, Debug, Deserialize, Serialize)] pub struct OpenFlowWPath { #[serde(flatten)] pub open_flow: OpenFlow, #[serde(default, skip_serializing_if = "Option::is_none")] pub dedicated_worker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_behalf_of_email: Option, pub path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tag: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, } /// Create flow request body #[derive(Clone, Debug, Serialize)] pub struct CreateFlowBody { #[serde(flatten)] pub open_flow_w_path: OpenFlowWPath, #[serde(default, skip_serializing_if = "Option::is_none")] pub deployment_message: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub draft_only: Option, } /// Flow response type #[derive(Clone, Debug, Deserialize)] pub struct Flow { pub path: String, #[serde(flatten)] pub open_flow: OpenFlow, #[serde(flatten)] pub extra: HashMap, } /// Workspace #[derive(Clone, Debug, Deserialize)] pub struct Workspace { pub id: String, pub name: String, #[serde(default)] pub owner: Option, #[serde(flatten)] pub extra: HashMap, } }