Skip to content

Commit ef49a86

Browse files
Add Vercel client starter kit
1 parent f2a1de7 commit ef49a86

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

src/adapters/vercel/mod.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use std::sync::OnceLock;
2+
3+
use derive_getters::Getters;
4+
use reqwest::{header, Client};
5+
use serde::{Deserialize, Serialize};
6+
use url::Url;
7+
8+
static URL: OnceLock<Url> = OnceLock::new();
9+
10+
fn init_url() -> Url {
11+
// NOTE: The trailing slash is important here, otherwise the URL parsing will fail!
12+
Url::parse("https://api.vercel.com/").unwrap()
13+
}
14+
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
#[serde(untagged)]
17+
pub enum TeamIdentifier {
18+
Id { team_id: String },
19+
Slug { slug: String },
20+
}
21+
22+
impl TeamIdentifier {
23+
pub fn id(id: impl Into<String>) -> Self {
24+
Self::Id { team_id: id.into() }
25+
}
26+
27+
pub fn slug(slug: impl Into<String>) -> Self {
28+
Self::Slug { slug: slug.into() }
29+
}
30+
}
31+
32+
#[derive(Clone, Getters)]
33+
pub struct VercelClient {
34+
http: Client,
35+
url: Url,
36+
team: Option<TeamIdentifier>,
37+
}
38+
39+
impl VercelClient {
40+
pub fn new(token: impl AsRef<str>, team: Option<TeamIdentifier>) -> anyhow::Result<Self> {
41+
let mut headers = header::HeaderMap::new();
42+
headers.insert(
43+
header::AUTHORIZATION,
44+
header::HeaderValue::from_str(&format!("Bearer {}", token.as_ref()))?,
45+
);
46+
headers.insert(
47+
header::CONTENT_TYPE,
48+
header::HeaderValue::from_static("application/json"),
49+
);
50+
51+
let http = Client::builder()
52+
.default_headers(headers)
53+
.build()?;
54+
55+
Ok(Self {
56+
http,
57+
url: URL.get_or_init(init_url).clone(),
58+
team,
59+
})
60+
}
61+
62+
fn add_team_query(&self, url: &mut Url) {
63+
if let Some(team) = &self.team {
64+
match team {
65+
TeamIdentifier::Id { team_id } => {
66+
url.query_pairs_mut().append_pair("teamId", team_id);
67+
}
68+
TeamIdentifier::Slug { slug } => {
69+
url.query_pairs_mut().append_pair("slug", slug);
70+
}
71+
}
72+
}
73+
}
74+
75+
pub async fn get_project(&self, id_or_name: impl AsRef<str>) -> anyhow::Result<Project> {
76+
let mut url = self.url.clone();
77+
url.set_path(&format!("v9/projects/{}", id_or_name.as_ref()));
78+
self.add_team_query(&mut url);
79+
80+
let response = self.http.get(url).send().await?;
81+
82+
if !response.status().is_success() {
83+
let status = response.status();
84+
let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
85+
anyhow::bail!("Failed to get project: {} - {}", status, text);
86+
}
87+
88+
let project = response.json::<Project>().await?;
89+
Ok(project)
90+
}
91+
}
92+
93+
// Response types for the project endpoint
94+
#[derive(Debug, Clone, Serialize, Deserialize)]
95+
#[serde(rename_all = "camelCase")]
96+
pub struct Project {
97+
pub id: String,
98+
pub name: String,
99+
pub account_id: String,
100+
pub updated_at: Option<u64>,
101+
pub created_at: u64,
102+
pub framework: Option<String>,
103+
pub dev_command: Option<String>,
104+
pub build_command: Option<String>,
105+
pub output_directory: Option<String>,
106+
pub root_directory: Option<String>,
107+
pub directory_listing: bool,
108+
pub node_version: String,
109+
pub live: Option<bool>,
110+
pub env: Option<Vec<EnvVariable>>,
111+
pub git_fork_protection: Option<bool>,
112+
pub git_lfs: Option<bool>,
113+
pub link: Option<serde_json::Value>,
114+
pub source_files_outside_root_directory: Option<bool>,
115+
}
116+
117+
#[derive(Debug, Clone, Serialize, Deserialize)]
118+
#[serde(rename_all = "camelCase")]
119+
pub struct EnvVariable {
120+
pub key: String,
121+
pub value: String,
122+
#[serde(rename = "type")]
123+
pub var_type: String,
124+
pub id: Option<String>,
125+
pub created_at: Option<u64>,
126+
pub updated_at: Option<u64>,
127+
pub target: Option<Vec<String>>,
128+
}

0 commit comments

Comments
 (0)