Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions generators/rust/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@fern-api/base-generator": "workspace:*",
"@fern-api/core-utils": "workspace:*",
"@fern-api/fs-utils": "workspace:*",
"@fern-api/logger": "workspace:*",
"@fern-fern/ir-sdk": "58.2.0",
"zod": "^3.22.4"
},
Expand Down
44 changes: 29 additions & 15 deletions generators/rust/base/src/asIs/api_client_builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::client::CLIENT_NAME;
use crate::{ApiError, ClientConfig};
use std::collections::HashMap;
use std::time::Duration;
use crate::{ClientConfig, ApiError};
use crate::client::{{CLIENT_NAME}};

/// Builder for creating API clients with custom configuration
pub struct ApiClientBuilder {
Expand All @@ -15,64 +15,78 @@ impl ApiClientBuilder {
config.base_url = base_url.into();
Self { config }
}

/// Set the API key for authentication
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.config.api_key = Some(key.into());
self
}

/// Set the bearer token for authentication
pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
self.config.bearer_token = Some(token.into());
self
}

/// Set the username for basic authentication
pub fn username(mut self, username: impl Into<String>) -> Self {
self.config.username = Some(username.into());
self
}

/// Set the password for basic authentication
pub fn password(mut self, password: impl Into<String>) -> Self {
self.config.password = Some(password.into());
self
}

/// Set the request timeout
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}

/// Set the maximum number of retries
pub fn max_retries(mut self, retries: u32) -> Self {
self.config.max_retries = retries;
self
}

/// Add a custom header
pub fn custom_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.custom_headers.insert(key.into(), value.into());
self
}

/// Add multiple custom headers
pub fn custom_headers(mut self, headers: HashMap<String, String>) -> Self {
self.config.custom_headers.extend(headers);
self
}

/// Set the user agent
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.config.user_agent = user_agent.into();
self
}

/// Build the client with validation
pub fn build(self) -> Result<{{CLIENT_NAME}}, ApiError> {
pub fn build(
self,
) -> Result<
{
{
CLIENT_NAME
}
},
ApiError,
> {
// Call the client constructor with all authentication parameters
{{CLIENT_NAME}}::new(self.config)
{
{
CLIENT_NAME
}
}
::new(self.config)
}
}
}
107 changes: 64 additions & 43 deletions generators/rust/base/src/asIs/http_client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::str::FromStr;
use reqwest::{Client, Request, Response, Method, header::{HeaderName, HeaderValue}};
use crate::{ApiError, ClientConfig, RequestOptions};
use reqwest::{
header::{HeaderName, HeaderValue},
Client, Method, Request, Response,
};
use serde::de::DeserializeOwned;
use crate::{ClientConfig, RequestOptions, ApiError};
use std::str::FromStr;

/// Internal HTTP client that handles requests with authentication and retries
#[derive(Clone)]
Expand All @@ -17,10 +20,10 @@ impl HttpClient {
.user_agent(&config.user_agent)
.build()
.map_err(ApiError::Network)?;

Ok(Self { client, config })
}

/// Execute a request with the given method, path, and options
pub async fn execute_request<T>(
&self,
Expand All @@ -33,101 +36,119 @@ impl HttpClient {
where
T: DeserializeOwned,
{
let url = format!("{}/{}",
self.config.base_url.trim_end_matches('/'),
let url = format!(
"{}/{}",
self.config.base_url.trim_end_matches('/'),
path.trim_start_matches('/')
);
let mut request = self.client.request(method, &url);

// Apply query parameters if provided
if let Some(params) = query_params {
request = request.query(&params);
}

// Apply body if provided
if let Some(body) = body {
request = request.json(&body);
}

// Build the request
let mut req = request.build().map_err(|e| ApiError::Network(e))?;

// Apply authentication and headers
self.apply_auth_headers(&mut req, &options)?;
self.apply_custom_headers(&mut req, &options)?;

// Execute with retries
let response = self.execute_with_retries(req, &options).await?;
self.parse_response(response).await
}

fn apply_auth_headers(&self, request: &mut Request, options: &Option<RequestOptions>) -> Result<(), ApiError> {

fn apply_auth_headers(
&self,
request: &mut Request,
options: &Option<RequestOptions>,
) -> Result<(), ApiError> {
let headers = request.headers_mut();

// Apply API key (request options override config)
let api_key = options.as_ref()
let api_key = options
.as_ref()
.and_then(|opts| opts.api_key.as_ref())
.or(self.config.api_key.as_ref());

if let Some(key) = api_key {
headers.insert("api_key", key.parse().map_err(|_| ApiError::InvalidHeader)?);
}

// Apply bearer token (request options override config)
let bearer_token = options.as_ref()
let bearer_token = options
.as_ref()
.and_then(|opts| opts.bearer_token.as_ref())
.or(self.config.bearer_token.as_ref());

if let Some(token) = bearer_token {
let auth_value = format!("Bearer {}", token);
headers.insert("Authorization", auth_value.parse().map_err(|_| ApiError::InvalidHeader)?);
headers.insert(
"Authorization",
auth_value.parse().map_err(|_| ApiError::InvalidHeader)?,
);
}

Ok(())
}

fn apply_custom_headers(&self, request: &mut Request, options: &Option<RequestOptions>) -> Result<(), ApiError> {

fn apply_custom_headers(
&self,
request: &mut Request,
options: &Option<RequestOptions>,
) -> Result<(), ApiError> {
let headers = request.headers_mut();

// Apply config-level custom headers
for (key, value) in &self.config.custom_headers {
headers.insert(
HeaderName::from_str(key).map_err(|_| ApiError::InvalidHeader)?,
HeaderValue::from_str(value).map_err(|_| ApiError::InvalidHeader)?
HeaderName::from_str(key).map_err(|_| ApiError::InvalidHeader)?,
HeaderValue::from_str(value).map_err(|_| ApiError::InvalidHeader)?,
);
}

// Apply request-level custom headers (override config)
if let Some(options) = options {
for (key, value) in &options.additional_headers {
headers.insert(
HeaderName::from_str(key).map_err(|_| ApiError::InvalidHeader)?,
HeaderValue::from_str(value).map_err(|_| ApiError::InvalidHeader)?
HeaderName::from_str(key).map_err(|_| ApiError::InvalidHeader)?,
HeaderValue::from_str(value).map_err(|_| ApiError::InvalidHeader)?,
);
}
}

Ok(())
}

async fn execute_with_retries(&self, request: Request, options: &Option<RequestOptions>) -> Result<Response, ApiError> {
let max_retries = options.as_ref()

async fn execute_with_retries(
&self,
request: Request,
options: &Option<RequestOptions>,
) -> Result<Response, ApiError> {
let max_retries = options
.as_ref()
.and_then(|opts| opts.max_retries)
.unwrap_or(self.config.max_retries);

let mut last_error = None;

for attempt in 0..=max_retries {
let cloned_request = request.try_clone()
.ok_or(ApiError::RequestClone)?;

let cloned_request = request.try_clone().ok_or(ApiError::RequestClone)?;

match self.client.execute(cloned_request).await {
Ok(response) if response.status().is_success() => return Ok(response),
Ok(response) => {
let status_code = response.status().as_u16();
let body = response.text().await.ok();
return Err(ApiError::from_response(status_code, body.as_deref()));
},
}
Err(e) if attempt < max_retries => {
last_error = Some(e);
// Exponential backoff
Expand All @@ -137,15 +158,15 @@ impl HttpClient {
Err(e) => return Err(ApiError::Network(e)),
}
}

Err(ApiError::Network(last_error.unwrap()))
}

async fn parse_response<T>(&self, response: Response) -> Result<T, ApiError>
where
T: DeserializeOwned,
{
let text = response.text().await.map_err(ApiError::Network)?;
serde_json::from_str(&text).map_err(ApiError::Serialization)
}
}
}
Loading
Loading