Skip to content

Commit ac9f487

Browse files
authored
Merge pull request #1 from opensass/api-key
feat: initial impl && test api key for the plebs
2 parents 8facbf4 + ca5e4c3 commit ac9f487

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed

Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "x-ai"
3+
version = "0.0.1"
4+
edition = "2021"
5+
6+
[dependencies]
7+
reqwest = { version = "0.12.9", features = ["json", "blocking"] }
8+
serde = { version = "1.0.215", features = ["derive"] }
9+
serde_json = "1.0.133"
10+
thiserror = "2.0.3"
11+
12+
[dev-dependencies]
13+
mockito = "1.6.1"
14+
tokio = { version = "1.41.1", features = ["full"] }

src/api_key.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Reference: https://docs.x.ai/api/endpoints#api-key
2+
3+
use crate::traits::ApiKeyFetcher;
4+
use crate::{error::XaiError, traits::ClientConfig};
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8+
pub struct ApiKeyInfo {
9+
pub acls: Vec<String>,
10+
pub api_key_blocked: bool,
11+
pub api_key_disabled: bool,
12+
pub api_key_id: String,
13+
pub create_time: String,
14+
pub modified_by: String,
15+
pub modify_time: String,
16+
pub name: String,
17+
pub redacted_api_key: String,
18+
pub team_blocked: bool,
19+
pub team_id: String,
20+
pub user_id: String,
21+
}
22+
23+
#[derive(Debug, Clone)]
24+
pub struct ApiKeyRequestBuilder<T: ClientConfig + Clone + Send + Sync> {
25+
client: T,
26+
}
27+
28+
impl<T> ApiKeyRequestBuilder<T>
29+
where
30+
T: ClientConfig + Clone + Send + Sync,
31+
{
32+
pub fn new(client: T) -> Self {
33+
Self { client }
34+
}
35+
}
36+
37+
impl<T> ApiKeyFetcher for ApiKeyRequestBuilder<T>
38+
where
39+
T: ClientConfig + Clone + Send + Sync,
40+
{
41+
async fn fetch_api_key_info(&self) -> Result<ApiKeyInfo, XaiError> {
42+
let response = self
43+
.client
44+
.request(reqwest::Method::GET, "api-key")?
45+
.send()
46+
.await?;
47+
48+
if response.status().is_success() {
49+
let api_key_info = response.json::<ApiKeyInfo>().await?;
50+
Ok(api_key_info)
51+
} else {
52+
Err(XaiError::Http(
53+
response.error_for_status().unwrap_err().to_string(),
54+
))
55+
}
56+
}
57+
}

src/client.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use crate::error::XaiError;
2+
use crate::traits::ClientConfig;
3+
use crate::XAI_V1_URL;
4+
use reqwest::{Client as HttpClient, Method, RequestBuilder};
5+
use std::sync::{Arc, RwLock};
6+
7+
#[derive(Clone, Debug)]
8+
pub struct XaiClient {
9+
http_client: Arc<HttpClient>,
10+
api_key: Arc<RwLock<Option<String>>>,
11+
base_url: String,
12+
}
13+
14+
impl XaiClient {
15+
pub fn builder() -> XaiClientBuilder {
16+
XaiClientBuilder::default()
17+
}
18+
}
19+
20+
impl ClientConfig for XaiClient {
21+
fn set_api_key(&self, api_key: String) {
22+
let mut key = self.api_key.write().unwrap();
23+
*key = Some(api_key);
24+
}
25+
26+
fn get_api_key(&self) -> Option<String> {
27+
self.api_key.read().unwrap().clone()
28+
}
29+
30+
fn request(&self, method: Method, endpoint: &str) -> Result<RequestBuilder, XaiError> {
31+
let api_key = self.get_api_key().ok_or(XaiError::MissingApiKey)?;
32+
33+
let url = format!("{}/{}", self.base_url, endpoint);
34+
let builder = self
35+
.http_client
36+
.request(method, &url)
37+
.header("Authorization", format!("Bearer {}", api_key));
38+
Ok(builder)
39+
}
40+
}
41+
42+
#[derive(Default, Debug)]
43+
pub struct XaiClientBuilder {
44+
base_url: Option<String>,
45+
}
46+
47+
impl XaiClientBuilder {
48+
pub fn base_url(mut self, url: &str) -> Self {
49+
self.base_url = Some(url.to_string());
50+
self
51+
}
52+
53+
pub fn build(self) -> Result<XaiClient, XaiError> {
54+
Ok(XaiClient {
55+
http_client: Arc::new(HttpClient::new()),
56+
api_key: Arc::new(RwLock::new(None)),
57+
base_url: self.base_url.unwrap_or_else(|| XAI_V1_URL.to_string()),
58+
})
59+
}
60+
}

src/error.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug, Clone)]
4+
pub enum XaiError {
5+
#[error("HTTP error: {0}")]
6+
Http(String),
7+
8+
#[error("Missing API key. Please set an API key before making requests.")]
9+
MissingApiKey,
10+
11+
#[error("Unexpected response format.")]
12+
UnexpectedResponseFormat,
13+
14+
#[error("Other error: {0}")]
15+
Other(String),
16+
}
17+
18+
impl From<reqwest::Error> for XaiError {
19+
fn from(err: reqwest::Error) -> Self {
20+
XaiError::Http(err.to_string())
21+
}
22+
}

src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod api_key;
2+
pub mod client;
3+
pub mod error;
4+
pub mod traits;
5+
6+
pub const XAI_V1_URL: &str = "https://api.x.ai/v1";

src/traits.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#![allow(async_fn_in_trait)]
2+
3+
use crate::api_key::ApiKeyInfo;
4+
use crate::error::XaiError;
5+
use reqwest::{Method, RequestBuilder};
6+
7+
pub trait ClientConfig {
8+
fn set_api_key(&self, api_key: String);
9+
fn get_api_key(&self) -> Option<String>;
10+
fn request(&self, method: Method, endpoint: &str) -> Result<RequestBuilder, XaiError>;
11+
}
12+
13+
pub trait ApiKeyFetcher {
14+
async fn fetch_api_key_info(&self) -> Result<ApiKeyInfo, XaiError>;
15+
}

tests/api_key.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use mockito::{Matcher, Server};
2+
use x_ai::api_key::ApiKeyRequestBuilder;
3+
use x_ai::client::XaiClient;
4+
use x_ai::traits::ApiKeyFetcher;
5+
use x_ai::traits::ClientConfig;
6+
7+
#[tokio::test]
8+
async fn test_fetch_api_key_info() {
9+
let mut server = Server::new_async().await;
10+
11+
let mock_response = r#"
12+
{
13+
"acls": ["api-key:model:*", "api-key:endpoint:*"],
14+
"api_key_blocked": false,
15+
"api_key_disabled": false,
16+
"api_key_id": "ae1e1841-4326-a8a9-8a1a7237db11",
17+
"create_time": "2024-01-01T12:55:18.139305Z",
18+
"modified_by": "3d38b4dc-4eb7-4785-ae26-c3fa8997ffc7",
19+
"modify_time": "2024-08-28T17:20:12.343321Z",
20+
"name": "My API Key",
21+
"redacted_api_key": "xG1k...b14o",
22+
"team_blocked": false,
23+
"team_id": "5ea6f6bd-7815-4b8a-9135-28b2d7ba6722",
24+
"user_id": "59fbe5f2-040b-46d5-8325-868bb8f23eb2"
25+
}
26+
"#;
27+
28+
let _mock = server
29+
.mock("GET", "/api-key")
30+
.match_header("Authorization", Matcher::Regex(r"^Bearer .*".to_string()))
31+
.with_status(200)
32+
.with_header("Content-Type", "application/json")
33+
.with_body(mock_response)
34+
.create_async()
35+
.await;
36+
37+
let client = XaiClient::builder()
38+
.base_url(&server.url())
39+
.build()
40+
.expect("Failed to build XaiClient");
41+
42+
client.set_api_key("test-api-key".to_string());
43+
44+
let request_builder = ApiKeyRequestBuilder::new(client);
45+
46+
let result = request_builder.fetch_api_key_info().await;
47+
48+
assert!(result.is_ok());
49+
50+
let api_key_info = result.unwrap();
51+
assert_eq!(api_key_info.api_key_id, "ae1e1841-4326-a8a9-8a1a7237db11");
52+
assert_eq!(api_key_info.name, "My API Key");
53+
assert!(!api_key_info.api_key_blocked);
54+
assert_eq!(
55+
api_key_info.acls,
56+
vec!["api-key:model:*", "api-key:endpoint:*"]
57+
);
58+
59+
server.reset();
60+
}

0 commit comments

Comments
 (0)