b3_utils/outcall.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
mod test;
mod cost;
pub use cost::*;
use ic_cdk::api::management_canister::http_request::{
http_request, http_request_with_closure, CanisterHttpRequestArgument, HttpHeader, HttpMethod,
TransformContext,
};
pub use ic_cdk::api::management_canister::http_request::HttpResponse as HttpOutcallResponse;
/// Used to build a request to the Management Canister's `http_request` method.
pub struct HttpOutcall(pub CanisterHttpRequestArgument);
impl HttpOutcall {
/// Creates a new request to be built up by having
pub fn new<S: AsRef<str>>(url: S) -> Self {
Self(CanisterHttpRequestArgument {
url: url.as_ref().to_string(),
headers: vec![],
method: HttpMethod::GET,
max_response_bytes: None,
transform: None,
body: None,
})
}
/// A simple wrapper to assign the URL with the `GET` method.
/// The `max_response_bytes` is set to the `max_response_bytes` argument.
/// The `max_response_bytes` argument is optional.
pub fn get(self, max_response_bytes: Option<u64>) -> Self {
self.method(HttpMethod::GET)
.max_response_bytes(max_response_bytes)
}
/// A simple wrapper to assign the URL with the `HEAD` method.
/// The `max_response_bytes` is set to the `max_response_bytes` argument.
/// The `max_response_bytes` argument is optional.
/// The `HEAD` method is used to retrieve the headers of the response.
/// The body of the response is empty.
pub fn head(self, max_response_bytes: Option<u64>) -> Self {
self.method(HttpMethod::HEAD)
.max_response_bytes(max_response_bytes)
}
/// A simple wrapper to assign the URL with the `POST` method.
/// The body is set to the `body` argument.
/// The `max_response_bytes` is set to the `max_response_bytes` argument.
/// The `max_response_bytes` argument is optional.
/// The Default `Content-Type` header is set to `application/json`.
/// The `Content-Type` header can be overwritten by using the `content_type` method.
pub fn post(self, body: &str, max_response_bytes: Option<u64>) -> Self {
self.method(HttpMethod::POST)
.add_headers(vec![(
"Content-Type".to_string(),
"application/json".to_string(),
)])
.max_response_bytes(max_response_bytes)
.body(body)
}
/// Updates the HTTP method.
pub fn method(mut self, http_method: HttpMethod) -> Self {
self.0.method = http_method;
self
}
/// Updates the body.
pub fn body(mut self, body: &str) -> Self {
self.0.body = Some(body.as_bytes().to_vec());
self
}
/// Adds HTTP headers for the request
pub fn add_headers(mut self, headers: Vec<(String, String)>) -> Self {
self.0
.headers
.extend(headers.iter().map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_string(),
}));
self
}
/// Sets the Content-Type header for the request
pub fn content_type(mut self, content_type: &str) -> Self {
// Remove any existing Content-Type headers
self.0
.headers
.retain(|header| header.name != "Content-Type");
// Add the new Content-Type header
self.0.headers.push(HttpHeader {
name: "Content-Type".to_string(),
value: content_type.to_string(),
});
self
}
/// add query params to the URL
pub fn add_query_params(mut self, params: Vec<(String, String)>) -> Self {
let query_string = params
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
if self.0.url.contains('?') {
self.0.url.push_str("&");
} else {
self.0.url.push_str("?");
}
self.0.url.push_str(&query_string);
self
}
/// Updates the transform context of the request.
pub fn transform_context(mut self, method: &str, context: Option<Vec<u8>>) -> Self {
let context = TransformContext::from_name(method.to_string(), context.unwrap_or_default());
self.0.transform = Some(context);
self
}
/// Updates the max_response_bytes of the request.
pub fn max_response_bytes(mut self, max_response_bytes: Option<u64>) -> Self {
self.0.max_response_bytes = max_response_bytes;
self
}
/// Calculate the cycle cost for this HTTP request
pub fn calculate_cycle_cost(&self) -> u128 {
HttpsOutcallCost::total(&self.0)
}
/// Wraps around `http_request` to issue a request to the `http_request` endpoint.
pub async fn send(self) -> Result<HttpOutcallResponse, String> {
let cycle_cost = self.calculate_cycle_cost();
http_request(self.0, cycle_cost)
.await
.map(|(response,)| response)
.map_err(|(_rejection_code, message)| message)
}
/// Wraps around `http_request_with_closure` to issue a request to the `http_request` endpoint with a transform closure.
pub async fn send_with_closure(
self,
transform_func: impl FnOnce(HttpOutcallResponse) -> HttpOutcallResponse + 'static,
) -> Result<HttpOutcallResponse, String> {
let cycle_cost = self.calculate_cycle_cost();
http_request_with_closure(self.0, cycle_cost, transform_func)
.await
.map(|(response,)| response)
.map_err(|(_rejection_code, message)| message)
}
}