1use base64::Engine;
6use bytes::{BufMut, Bytes, BytesMut};
7use futures_util::{Stream, StreamExt};
8use reqwest::{Client, Response, StatusCode};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::pin::Pin;
13use std::time::Duration;
14use thiserror::Error;
15use url::Url;
16
17#[derive(Debug, Clone, Deserialize)]
19pub struct FunctionErrorDetails {
20 pub message: Option<String>,
21 pub status: Option<u16>,
22 pub code: Option<String>,
23 pub details: Option<Value>,
24}
25
26#[derive(Debug, Error)]
28pub enum FunctionsError {
29 #[error("Request error: {0}")]
30 RequestError(#[from] reqwest::Error),
31
32 #[error("URL parse error: {0}")]
33 UrlError(#[from] url::ParseError),
34
35 #[error("JSON error: {0}")]
36 JsonError(#[from] serde_json::Error),
37
38 #[error("Function error (status: {status}): {message}")]
39 FunctionError {
40 message: String,
41 status: StatusCode,
42 details: Option<FunctionErrorDetails>,
43 },
44
45 #[error("Timeout error: Function execution exceeded timeout limit")]
46 TimeoutError,
47
48 #[error("Invalid response: {0}")]
49 InvalidResponse(String),
50}
51
52impl FunctionsError {
53 pub fn new(message: String) -> Self {
54 Self::FunctionError {
55 message,
56 status: StatusCode::INTERNAL_SERVER_ERROR,
57 details: None,
58 }
59 }
60
61 pub fn from_response(response: &Response) -> Self {
62 Self::FunctionError {
63 message: format!("Function returned error status: {}", response.status()),
64 status: response.status(),
65 details: None,
66 }
67 }
68
69 pub fn with_details(response: &Response, details: FunctionErrorDetails) -> Self {
70 Self::FunctionError {
71 message: details.message.as_ref().map_or_else(
72 || format!("Function returned error status: {}", response.status()),
73 |msg| msg.clone(),
74 ),
75 status: response.status(),
76 details: Some(details),
77 }
78 }
79}
80
81pub type Result<T> = std::result::Result<T, FunctionsError>;
82
83#[derive(Clone, Debug)]
85pub struct FunctionOptions {
86 pub headers: Option<HashMap<String, String>>,
88
89 pub timeout_seconds: Option<u64>,
91
92 pub response_type: ResponseType,
94
95 pub content_type: Option<String>,
97}
98
99impl Default for FunctionOptions {
100 fn default() -> Self {
101 Self {
102 headers: None,
103 timeout_seconds: None,
104 response_type: ResponseType::Json,
105 content_type: None,
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ResponseType {
113 Json,
115
116 Text,
118
119 Binary,
121
122 Stream,
124}
125
126pub type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>;
128
129#[derive(Debug, Clone)]
131pub struct FunctionResponse<T> {
132 pub data: T,
134
135 pub status: StatusCode,
137
138 pub headers: HashMap<String, String>,
140}
141
142pub struct FunctionsClient {
144 base_url: String,
145 api_key: String,
146 http_client: Client,
147}
148
149pub struct FunctionRequest<'a, T> {
151 client: &'a FunctionsClient,
152 function_name: String,
153 _response_type: std::marker::PhantomData<T>,
154}
155
156impl<'a, T: DeserializeOwned> FunctionRequest<'a, T> {
157 pub async fn execute<B: Serialize>(
159 &self,
160 body: Option<B>,
161 options: Option<FunctionOptions>,
162 ) -> Result<T> {
163 let result = self
164 .client
165 .invoke::<T, B>(&self.function_name, body, options)
166 .await?;
167 Ok(result.data)
168 }
169}
170
171impl FunctionsClient {
172 pub fn new(supabase_url: &str, supabase_key: &str, http_client: Client) -> Self {
174 Self {
175 base_url: supabase_url.to_string(),
176 api_key: supabase_key.to_string(),
177 http_client,
178 }
179 }
180
181 pub async fn invoke<T: DeserializeOwned, B: Serialize>(
183 &self,
184 function_name: &str,
185 body: Option<B>,
186 options: Option<FunctionOptions>,
187 ) -> Result<FunctionResponse<T>> {
188 let opts = options.unwrap_or_default();
189
190 let mut url = Url::parse(&self.base_url)?;
192 url.path_segments_mut()
193 .map_err(|_| FunctionsError::UrlError(url::ParseError::EmptyHost))?
194 .push("functions")
195 .push("v1")
196 .push(function_name);
197
198 let mut request_builder = self
200 .http_client
201 .post(url)
202 .header("apikey", &self.api_key)
203 .header("Authorization", format!("Bearer {}", &self.api_key));
204
205 if let Some(timeout) = opts.timeout_seconds {
207 request_builder = request_builder.timeout(Duration::from_secs(timeout));
208 }
209
210 if let Some(content_type) = opts.content_type {
212 request_builder = request_builder.header("Content-Type", content_type);
213 }
214
215 if let Some(headers) = opts.headers {
217 for (key, value) in headers {
218 request_builder = request_builder.header(key, value);
219 }
220 }
221
222 if let Some(body_data) = body {
224 request_builder = request_builder.json(&body_data);
225 }
226
227 let response = request_builder.send().await.map_err(|e| {
229 if e.is_timeout() {
230 FunctionsError::TimeoutError
231 } else {
232 FunctionsError::from(e)
233 }
234 })?;
235
236 let status = response.status();
238 if !status.is_success() {
239 let status_copy = status;
241
242 let error_body = response
244 .text()
245 .await
246 .unwrap_or_else(|_| "Failed to read error response".to_string());
247
248 if let Ok(error_details) = serde_json::from_str::<FunctionErrorDetails>(&error_body) {
249 return Err(FunctionsError::FunctionError {
250 message: error_details.message.as_ref().map_or_else(
251 || format!("Function returned error status: {}", status_copy),
252 |msg| msg.clone(),
253 ),
254 status: status_copy,
255 details: Some(error_details),
256 });
257 } else {
258 return Err(FunctionsError::FunctionError {
259 message: error_body,
260 status: status_copy,
261 details: None,
262 });
263 }
264 }
265
266 let headers = response
268 .headers()
269 .iter()
270 .map(|(name, value)| (name.to_string(), value.to_str().unwrap_or("").to_string()))
271 .collect::<HashMap<String, String>>();
272
273 match opts.response_type {
275 ResponseType::Json => {
276 let data = response.json::<T>().await.map_err(|e| {
277 FunctionsError::JsonError(serde_json::from_str::<T>("{}").err().unwrap_or_else(
278 || {
279 serde_json::Error::io(std::io::Error::new(
280 std::io::ErrorKind::InvalidData,
281 e.to_string(),
282 ))
283 },
284 ))
285 })?;
286
287 Ok(FunctionResponse {
288 data,
289 status,
290 headers,
291 })
292 }
293 ResponseType::Text => {
294 let text = response.text().await?;
296
297 let data: T = serde_json::from_str(&text).unwrap_or_else(|_| {
299 panic!("Failed to deserialize text response as requested type")
300 });
301
302 Ok(FunctionResponse {
303 data,
304 status,
305 headers,
306 })
307 }
308 ResponseType::Binary => {
309 let bytes = response.bytes().await?;
311
312 let binary_str = base64::engine::general_purpose::STANDARD.encode(&bytes);
314
315 let data: T =
317 serde_json::from_str(&format!("\"{}\"", binary_str)).unwrap_or_else(|_| {
318 panic!("Failed to deserialize binary response as requested type")
319 });
320
321 Ok(FunctionResponse {
322 data,
323 status,
324 headers,
325 })
326 }
327 ResponseType::Stream => {
328 Err(FunctionsError::InvalidResponse(
331 "Stream response type cannot be handled by invoke(). Use invoke_stream() instead.".to_string()
332 ))
333 }
334 }
335 }
336
337 pub async fn invoke_json<T: DeserializeOwned, B: Serialize>(
339 &self,
340 function_name: &str,
341 body: Option<B>,
342 ) -> Result<T> {
343 let options = FunctionOptions {
344 response_type: ResponseType::Json,
345 ..Default::default()
346 };
347
348 let response = self
349 .invoke::<T, B>(function_name, body, Some(options))
350 .await?;
351 Ok(response.data)
352 }
353
354 pub async fn invoke_text<B: Serialize>(
356 &self,
357 function_name: &str,
358 body: Option<B>,
359 ) -> Result<String> {
360 let options = FunctionOptions {
361 response_type: ResponseType::Text,
362 ..Default::default()
363 };
364
365 let mut url = Url::parse(&self.base_url)?;
367 url.path_segments_mut()
368 .map_err(|_| FunctionsError::UrlError(url::ParseError::EmptyHost))?
369 .push("functions")
370 .push("v1")
371 .push(function_name);
372
373 let mut request_builder = self
375 .http_client
376 .post(url)
377 .header("apikey", &self.api_key)
378 .header("Authorization", format!("Bearer {}", &self.api_key));
379
380 if let Some(timeout) = options.timeout_seconds {
382 request_builder = request_builder.timeout(Duration::from_secs(timeout));
383 }
384
385 if let Some(content_type) = options.content_type {
387 request_builder = request_builder.header("Content-Type", content_type);
388 } else {
389 request_builder = request_builder.header("Content-Type", "application/json");
390 }
391
392 request_builder = request_builder.header("Accept", "text/plain, */*;q=0.9");
394
395 if let Some(headers) = options.headers {
397 for (key, value) in headers {
398 request_builder = request_builder.header(key, value);
399 }
400 }
401
402 if let Some(body_data) = body {
404 request_builder = request_builder.json(&body_data);
405 }
406
407 let response = request_builder.send().await.map_err(|e| {
409 if e.is_timeout() {
410 FunctionsError::TimeoutError
411 } else {
412 FunctionsError::from(e)
413 }
414 })?;
415
416 let status = response.status();
418 if !status.is_success() {
419 let error_body = response
421 .text()
422 .await
423 .unwrap_or_else(|_| "Failed to read error response".to_string());
424 if let Ok(error_details) = serde_json::from_str::<FunctionErrorDetails>(&error_body) {
425 return Err(FunctionsError::FunctionError {
426 message: error_details.message.as_ref().map_or_else(
427 || format!("Function returned error status: {}", status),
428 |msg| msg.clone(),
429 ),
430 status,
431 details: Some(error_details),
432 });
433 } else {
434 return Err(FunctionsError::FunctionError {
435 message: error_body,
436 status,
437 details: None,
438 });
439 }
440 }
441
442 response.text().await.map_err(FunctionsError::from)
444 }
445
446 pub async fn invoke_binary<B: Serialize>(
448 &self,
449 function_name: &str,
450 body: Option<B>,
451 options: Option<FunctionOptions>,
452 ) -> Result<Bytes> {
453 let options = options.unwrap_or_else(|| FunctionOptions {
454 response_type: ResponseType::Binary,
455 ..Default::default()
456 });
457
458 let mut url = Url::parse(&self.base_url)?;
460 url.path_segments_mut()
461 .map_err(|_| FunctionsError::UrlError(url::ParseError::EmptyHost))?
462 .push("functions")
463 .push("v1")
464 .push(function_name);
465
466 let mut request_builder = self
468 .http_client
469 .post(url)
470 .header("apikey", &self.api_key)
471 .header("Authorization", format!("Bearer {}", &self.api_key));
472
473 if let Some(timeout) = options.timeout_seconds {
475 request_builder = request_builder.timeout(Duration::from_secs(timeout));
476 }
477
478 if let Some(content_type) = options.content_type {
480 request_builder = request_builder.header("Content-Type", content_type);
481 } else {
482 request_builder = request_builder.header("Content-Type", "application/json");
484 }
485
486 request_builder = request_builder.header("Accept", "application/octet-stream");
488
489 if let Some(headers) = options.headers {
491 for (key, value) in headers {
492 request_builder = request_builder.header(key, value);
493 }
494 }
495
496 if let Some(body_data) = body {
498 request_builder = request_builder.json(&body_data);
499 }
500
501 let response = request_builder.send().await.map_err(|e| {
503 if e.is_timeout() {
504 FunctionsError::TimeoutError
505 } else {
506 FunctionsError::from(e)
507 }
508 })?;
509
510 let status = response.status();
512 if !status.is_success() {
513 let error_body = response
515 .text()
516 .await
517 .unwrap_or_else(|_| "Failed to read error response".to_string());
518
519 if let Ok(error_details) = serde_json::from_str::<FunctionErrorDetails>(&error_body) {
520 return Err(FunctionsError::FunctionError {
521 message: error_details.message.as_ref().map_or_else(
522 || format!("Function returned error status: {}", status),
523 |msg| msg.clone(),
524 ),
525 status,
526 details: Some(error_details),
527 });
528 } else {
529 return Err(FunctionsError::FunctionError {
530 message: error_body,
531 status,
532 details: None,
533 });
534 }
535 }
536
537 response.bytes().await.map_err(FunctionsError::from)
539 }
540
541 pub async fn invoke_binary_stream<B: Serialize>(
543 &self,
544 function_name: &str,
545 body: Option<B>,
546 options: Option<FunctionOptions>,
547 ) -> Result<ByteStream> {
548 let opts = options.unwrap_or_else(|| FunctionOptions {
549 response_type: ResponseType::Stream,
550 content_type: Some("application/octet-stream".to_string()),
551 ..Default::default()
552 });
553
554 let mut custom_opts = opts;
555 let mut headers = custom_opts.headers.unwrap_or_default();
556 headers.insert("Accept".to_string(), "application/octet-stream".to_string());
557 custom_opts.headers = Some(headers);
558
559 self.invoke_stream(function_name, body, Some(custom_opts))
560 .await
561 }
562
563 pub fn process_binary_chunks<F>(
565 &self,
566 stream: ByteStream,
567 chunk_size: usize,
568 mut processor: F,
569 ) -> Pin<Box<dyn Stream<Item = Result<Bytes>> + Send + '_>>
570 where
571 F: FnMut(&[u8]) -> std::result::Result<Bytes, String> + Send + 'static,
572 {
573 Box::pin(async_stream::stream! {
574 let mut buffer = BytesMut::new();
575
576 tokio::pin!(stream);
577 while let Some(chunk_result) = stream.next().await {
578 match chunk_result {
579 Ok(chunk) => {
580 buffer.extend_from_slice(&chunk);
582
583 while buffer.len() >= chunk_size {
585 let chunk_to_process = buffer.split_to(chunk_size);
586 match processor(&chunk_to_process) {
587 Ok(processed) => yield Ok(processed),
588 Err(err) => {
589 yield Err(FunctionsError::InvalidResponse(err));
590 return;
591 }
592 }
593 }
594 },
595 Err(e) => {
596 yield Err(e);
597 return;
598 }
599 }
600 }
601
602 if !buffer.is_empty() {
604 match processor(&buffer) {
605 Ok(processed) => yield Ok(processed),
606 Err(err) => yield Err(FunctionsError::InvalidResponse(err)),
607 }
608 }
609 })
610 }
611
612 pub async fn invoke_stream<B: Serialize>(
614 &self,
615 function_name: &str,
616 body: Option<B>,
617 options: Option<FunctionOptions>,
618 ) -> Result<ByteStream> {
619 let opts = options.unwrap_or_else(|| FunctionOptions {
620 response_type: ResponseType::Stream,
621 ..Default::default()
622 });
623
624 let mut url = Url::parse(&self.base_url)?;
626 url.path_segments_mut()
627 .map_err(|_| FunctionsError::UrlError(url::ParseError::EmptyHost))?
628 .push("functions")
629 .push("v1")
630 .push(function_name);
631
632 let mut request_builder = self
634 .http_client
635 .post(url)
636 .header("apikey", &self.api_key)
637 .header("Authorization", format!("Bearer {}", &self.api_key));
638
639 if let Some(timeout) = opts.timeout_seconds {
641 request_builder = request_builder.timeout(Duration::from_secs(timeout));
642 }
643
644 if let Some(content_type) = opts.content_type {
646 request_builder = request_builder.header("Content-Type", content_type);
647 } else {
648 request_builder = request_builder.header("Content-Type", "application/json");
650 }
651
652 if let Some(headers) = opts.headers {
654 for (key, value) in headers {
655 request_builder = request_builder.header(key, value);
656 }
657 }
658
659 if let Some(body_data) = body {
661 request_builder = request_builder.json(&body_data);
662 }
663
664 let response = request_builder.send().await.map_err(|e| {
666 if e.is_timeout() {
667 FunctionsError::TimeoutError
668 } else {
669 FunctionsError::from(e)
670 }
671 })?;
672
673 let status = response.status();
675 if !status.is_success() {
676 let status_copy = status;
678
679 let error_body = response
681 .text()
682 .await
683 .unwrap_or_else(|_| "Failed to read error response".to_string());
684
685 if let Ok(error_details) = serde_json::from_str::<FunctionErrorDetails>(&error_body) {
686 return Err(FunctionsError::FunctionError {
687 message: error_details.message.as_ref().map_or_else(
688 || format!("Function returned error status: {}", status_copy),
689 |msg| msg.clone(),
690 ),
691 status: status_copy,
692 details: Some(error_details),
693 });
694 } else {
695 return Err(FunctionsError::FunctionError {
696 message: error_body,
697 status: status_copy,
698 details: None,
699 });
700 }
701 }
702
703 Ok(Box::pin(
705 response
706 .bytes_stream()
707 .map(|result| result.map_err(FunctionsError::from)),
708 ))
709 }
710
711 pub async fn invoke_json_stream<B: Serialize>(
713 &self,
714 function_name: &str,
715 body: Option<B>,
716 options: Option<FunctionOptions>,
717 ) -> Result<Pin<Box<dyn Stream<Item = Result<Value>> + Send + '_>>> {
718 let byte_stream = self.invoke_stream(function_name, body, options).await?;
719 let json_stream = self.byte_stream_to_json(byte_stream);
720 Ok(json_stream)
721 }
722
723 fn byte_stream_to_json(
725 &self,
726 stream: ByteStream,
727 ) -> Pin<Box<dyn Stream<Item = Result<Value>> + Send + '_>> {
728 Box::pin(async_stream::stream! {
729 let mut line_stream = self.stream_to_lines(stream);
730
731 while let Some(line_result) = line_stream.next().await {
732 match line_result {
733 Ok(line) => {
734 if line.trim().is_empty() {
736 continue;
737 }
738
739 match serde_json::from_str::<Value>(&line) {
741 Ok(json_value) => {
742 yield Ok(json_value);
743 },
744 Err(err) => {
745 yield Err(FunctionsError::JsonError(err));
746 }
747 }
748 },
749 Err(err) => {
750 yield Err(err);
751 break;
752 }
753 }
754 }
755 })
756 }
757
758 pub fn stream_to_lines(
760 &self,
761 stream: ByteStream,
762 ) -> Pin<Box<dyn Stream<Item = Result<String>> + Send + '_>> {
763 Box::pin(async_stream::stream! {
764 let mut buf = BytesMut::new();
765
766 tokio::pin!(stream);
768 while let Some(chunk_result) = stream.next().await {
769 match chunk_result {
770 Ok(chunk) => {
771 buf.extend_from_slice(&chunk);
772
773 while let Some(i) = buf.iter().position(|&b| b == b'\n') {
775 let line = if i > 0 && buf[i - 1] == b'\r' {
776 let line = String::from_utf8_lossy(&buf[..i - 1]).to_string();
778 unsafe { buf.advance_mut(i + 1); }
779 line
780 } else {
781 let line = String::from_utf8_lossy(&buf[..i]).to_string();
783 unsafe { buf.advance_mut(i + 1); }
784 line
785 };
786
787 yield Ok(line);
788 }
789 },
790 Err(e) => {
791 yield Err(e);
792 break;
793 }
794 }
795 }
796
797 if !buf.is_empty() {
799 let line = String::from_utf8_lossy(&buf).to_string();
800 yield Ok(line);
801 }
802 })
803 }
804
805 pub fn create_request<T: DeserializeOwned>(
807 &self,
808 function_name: &str,
809 ) -> FunctionRequest<'_, T> {
810 FunctionRequest {
811 client: self,
812 function_name: function_name.to_string(),
813 _response_type: std::marker::PhantomData,
814 }
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*; use serde_json::json;
822 use wiremock::matchers::{body_json, header, method, path};
823 use wiremock::{Mock, MockServer, ResponseTemplate};
824
825 #[derive(Debug, Serialize, Deserialize, PartialEq)]
827 struct TestPayload {
828 message: String,
829 }
830
831 #[tokio::test]
832 async fn test_invoke() {
833 }
835
836 #[tokio::test]
838 async fn test_invoke_json_success() {
839 let server = MockServer::start().await;
841 let mock_uri = server.uri();
842 let api_key = "test-key";
843 let function_name = "hello-world";
844
845 let request_body = json!({ "name": "Rust" });
847 let expected_response = TestPayload {
848 message: "Hello Rust".to_string(),
849 };
850
851 Mock::given(method("POST"))
853 .and(path(format!("/functions/v1/{}", function_name)))
854 .and(header("apikey", api_key))
855 .and(header(
856 "Authorization",
857 format!("Bearer {}", api_key).as_str(),
858 ))
859 .and(header("Content-Type", "application/json"))
860 .and(body_json(&request_body))
861 .respond_with(ResponseTemplate::new(200).set_body_json(&expected_response))
862 .mount(&server)
863 .await;
864
865 let client = FunctionsClient::new(&mock_uri, api_key, reqwest::Client::new());
867 let result = client
868 .invoke_json::<TestPayload, Value>(function_name, Some(request_body))
869 .await;
870
871 assert!(result.is_ok());
873 let data = result.unwrap();
874 assert_eq!(data, expected_response);
875 server.verify().await;
876 }
877
878 #[tokio::test]
880 async fn test_invoke_json_error_with_details() {
881 let server = MockServer::start().await;
883 let mock_uri = server.uri();
884 let api_key = "test-key";
885 let function_name = "error-func";
886
887 let request_body = json!({ "input": "invalid" });
889 let error_response_body = json!({
890 "message": "Something went wrong!",
891 "code": "FUNC_ERROR",
892 "details": { "reason": "Internal failure" }
893 });
894
895 Mock::given(method("POST"))
897 .and(path(format!("/functions/v1/{}", function_name)))
898 .and(header("apikey", api_key))
899 .and(header(
900 "Authorization",
901 format!("Bearer {}", api_key).as_str(),
902 ))
903 .and(body_json(&request_body))
904 .respond_with(
905 ResponseTemplate::new(500)
906 .set_body_json(&error_response_body)
907 .insert_header("Content-Type", "application/json"),
908 )
909 .mount(&server)
910 .await;
911
912 let client = FunctionsClient::new(&mock_uri, api_key, reqwest::Client::new());
914 let result = client
917 .invoke_json::<Value, Value>(function_name, Some(request_body))
918 .await;
919
920 assert!(result.is_err());
922 match result.err().unwrap() {
923 FunctionsError::FunctionError {
924 message,
925 status,
926 details,
927 } => {
928 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
929 assert_eq!(message, "Something went wrong!");
930 assert!(details.is_some());
931 let details_unwrapped = details.unwrap();
932 assert_eq!(
933 details_unwrapped.message,
934 Some("Something went wrong!".to_string())
935 );
936 assert_eq!(details_unwrapped.code, Some("FUNC_ERROR".to_string()));
937 assert!(details_unwrapped.details.is_some());
938 assert_eq!(
939 details_unwrapped.details.unwrap(),
940 json!({ "reason": "Internal failure" })
941 );
942 }
943 _ => panic!("Expected FunctionError, got different error type"),
944 }
945 server.verify().await;
946 }
947
948 #[tokio::test]
950 async fn test_invoke_text_success() {
951 let server = MockServer::start().await;
953 let mock_uri = server.uri();
954 let api_key = "test-key";
955 let function_name = "plain-text-func";
956
957 let request_body = json!({ "format": "text" });
959 let expected_response_text = "This is a plain text response.";
960
961 Mock::given(method("POST"))
963 .and(path(format!("/functions/v1/{}", function_name)))
964 .and(header("apikey", api_key))
965 .and(header(
966 "Authorization",
967 format!("Bearer {}", api_key).as_str(),
968 ))
969 .and(header("Content-Type", "application/json")) .and(body_json(&request_body))
971 .respond_with(
972 ResponseTemplate::new(200)
973 .set_body_string(expected_response_text)
974 .insert_header("Content-Type", "text/plain"), )
976 .mount(&server)
977 .await;
978
979 let client = FunctionsClient::new(&mock_uri, api_key, reqwest::Client::new());
981 let result = client
982 .invoke_text::<Value>(function_name, Some(request_body)) .await;
984
985 assert!(result.is_ok());
987 let data = result.unwrap();
988 assert_eq!(data, expected_response_text);
989 server.verify().await;
990 }
991}