alien_bindings/providers/function/
gcp_cloudrun.rs1use crate::error::{ErrorData, Result};
2use crate::traits::{Binding, Function, FunctionInvokeRequest, FunctionInvokeResponse};
3use alien_core::bindings::CloudRunFunctionBinding;
4use alien_error::{AlienError, Context, IntoAlienError};
5use alien_gcp_clients::cloudrun::{CloudRunApi, CloudRunClient};
6use alien_gcp_clients::GcpClientConfig;
7use async_trait::async_trait;
8use reqwest::Client;
9use std::collections::BTreeMap;
10
11#[derive(Debug)]
13pub struct CloudRunFunction {
14 client: Client,
15 cloudrun_client: CloudRunClient,
16 binding: CloudRunFunctionBinding,
17}
18
19impl CloudRunFunction {
20 pub fn new(client: Client, config: GcpClientConfig, binding: CloudRunFunctionBinding) -> Self {
21 let cloudrun_client = CloudRunClient::new(client.clone(), config);
22 Self {
23 client,
24 cloudrun_client,
25 binding,
26 }
27 }
28
29 fn get_private_url(&self) -> Result<String> {
31 self.binding
32 .private_url
33 .clone()
34 .into_value("function", "private_url")
35 .context(ErrorData::BindingConfigInvalid {
36 binding_name: "function".to_string(),
37 reason: "Failed to resolve private_url from binding".to_string(),
38 })
39 }
40
41 async fn resolve_target_url(&self, target_function: &str) -> Result<String> {
43 if !target_function.is_empty() {
44 if target_function.starts_with("http://") || target_function.starts_with("https://") {
46 Ok(target_function.to_string())
48 } else {
49 self.get_private_url()
51 }
52 } else {
53 self.get_private_url()
55 }
56 }
57}
58
59impl Binding for CloudRunFunction {}
60
61#[async_trait]
62impl Function for CloudRunFunction {
63 async fn invoke(&self, request: FunctionInvokeRequest) -> Result<FunctionInvokeResponse> {
64 let target_url = self.resolve_target_url(&request.target_function).await?;
65
66 let url = if request.path.starts_with('/') {
68 format!("{}{}", target_url.trim_end_matches('/'), request.path)
69 } else {
70 format!("{}/{}", target_url.trim_end_matches('/'), request.path)
71 };
72
73 let method = match request.method.to_uppercase().as_str() {
75 "GET" => reqwest::Method::GET,
76 "POST" => reqwest::Method::POST,
77 "PUT" => reqwest::Method::PUT,
78 "DELETE" => reqwest::Method::DELETE,
79 "PATCH" => reqwest::Method::PATCH,
80 "HEAD" => reqwest::Method::HEAD,
81 "OPTIONS" => reqwest::Method::OPTIONS,
82 _ => {
83 return Err(AlienError::new(ErrorData::InvalidInput {
84 operation_context: "Function invocation".to_string(),
85 details: format!("Unsupported HTTP method: {}", request.method),
86 field_name: Some("method".to_string()),
87 }));
88 }
89 };
90
91 let mut req_builder = self.client.request(method, &url);
92
93 for (key, value) in &request.headers {
95 req_builder = req_builder.header(key, value);
96 }
97
98 if !request.body.is_empty() {
100 req_builder = req_builder.body(request.body.clone());
101 }
102
103 if let Some(timeout) = request.timeout {
105 req_builder = req_builder.timeout(timeout);
106 }
107
108 let response =
110 req_builder
111 .send()
112 .await
113 .into_alien_error()
114 .context(ErrorData::HttpRequestFailed {
115 url: url.clone(),
116 method: request.method.clone(),
117 })?;
118
119 let status = response.status().as_u16();
121
122 let headers = response
123 .headers()
124 .iter()
125 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
126 .collect::<BTreeMap<String, String>>();
127
128 let body = response
129 .bytes()
130 .await
131 .into_alien_error()
132 .context(ErrorData::HttpRequestFailed {
133 url: url.clone(),
134 method: "READ_BODY".to_string(),
135 })?
136 .to_vec();
137
138 Ok(FunctionInvokeResponse {
139 status,
140 headers,
141 body,
142 })
143 }
144
145 async fn get_function_url(&self) -> Result<Option<String>> {
146 if let Some(url_binding) = &self.binding.public_url {
148 let url = url_binding
149 .clone()
150 .into_value("function", "public_url")
151 .context(ErrorData::BindingConfigInvalid {
152 binding_name: "function".to_string(),
153 reason: "Failed to resolve public_url from binding".to_string(),
154 })?;
155 return Ok(Some(url));
156 }
157
158 let service_name = self
160 .binding
161 .service_name
162 .clone()
163 .into_value("function", "service_name")
164 .context(ErrorData::BindingConfigInvalid {
165 binding_name: "function".to_string(),
166 reason: "Failed to resolve service_name from binding".to_string(),
167 })?;
168
169 let location = self
170 .binding
171 .location
172 .clone()
173 .into_value("function", "location")
174 .context(ErrorData::BindingConfigInvalid {
175 binding_name: "function".to_string(),
176 reason: "Failed to resolve location from binding".to_string(),
177 })?;
178
179 match self
180 .cloudrun_client
181 .get_service(location, service_name)
182 .await
183 {
184 Ok(service) => {
185 Ok(service.urls.first().cloned())
187 }
188 Err(_) => Ok(None), }
190 }
191
192 fn as_any(&self) -> &dyn std::any::Any {
193 self
194 }
195}