Add cloud_api_client
and cloud_api_types
crates (#35357)
This PR adds two new crates for interacting with Cloud: - `cloud_api_client` - The client that will be used to talk to Cloud. - `cloud_api_types` - The types for the Cloud API that are shared between Zed and Cloud. Release Notes: - N/A
This commit is contained in:
parent
7695c4b82e
commit
bc6bb42745
11 changed files with 177 additions and 0 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -2976,6 +2976,7 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clock",
|
"clock",
|
||||||
|
"cloud_api_client",
|
||||||
"cloud_llm_client",
|
"cloud_llm_client",
|
||||||
"cocoa 0.26.0",
|
"cocoa 0.26.0",
|
||||||
"collections",
|
"collections",
|
||||||
|
@ -3031,6 +3032,27 @@ dependencies = [
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloud_api_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cloud_api_types",
|
||||||
|
"futures 0.3.31",
|
||||||
|
"http_client",
|
||||||
|
"parking_lot",
|
||||||
|
"serde_json",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cloud_api_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloud_llm_client"
|
name = "cloud_llm_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -29,6 +29,8 @@ members = [
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/clock",
|
"crates/clock",
|
||||||
|
"crates/cloud_api_client",
|
||||||
|
"crates/cloud_api_types",
|
||||||
"crates/cloud_llm_client",
|
"crates/cloud_llm_client",
|
||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
|
@ -251,6 +253,8 @@ channel = { path = "crates/channel" }
|
||||||
cli = { path = "crates/cli" }
|
cli = { path = "crates/cli" }
|
||||||
client = { path = "crates/client" }
|
client = { path = "crates/client" }
|
||||||
clock = { path = "crates/clock" }
|
clock = { path = "crates/clock" }
|
||||||
|
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||||
|
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||||
collab = { path = "crates/collab" }
|
collab = { path = "crates/collab" }
|
||||||
collab_ui = { path = "crates/collab_ui" }
|
collab_ui = { path = "crates/collab_ui" }
|
||||||
|
|
|
@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
cloud_api_client.workspace = true
|
||||||
cloud_llm_client.workspace = true
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
credentials_provider.workspace = true
|
credentials_provider.workspace = true
|
||||||
|
|
|
@ -15,6 +15,7 @@ use async_tungstenite::tungstenite::{
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use clock::SystemClock;
|
use clock::SystemClock;
|
||||||
|
use cloud_api_client::CloudApiClient;
|
||||||
use credentials_provider::CredentialsProvider;
|
use credentials_provider::CredentialsProvider;
|
||||||
use futures::{
|
use futures::{
|
||||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||||
|
@ -213,6 +214,7 @@ pub struct Client {
|
||||||
id: AtomicU64,
|
id: AtomicU64,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<HttpClientWithUrl>,
|
http: Arc<HttpClientWithUrl>,
|
||||||
|
cloud_client: Arc<CloudApiClient>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
credentials_provider: ClientCredentialsProvider,
|
credentials_provider: ClientCredentialsProvider,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
|
@ -586,6 +588,7 @@ impl Client {
|
||||||
id: AtomicU64::new(0),
|
id: AtomicU64::new(0),
|
||||||
peer: Peer::new(0),
|
peer: Peer::new(0),
|
||||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||||
|
cloud_client: Arc::new(CloudApiClient::new(http.clone())),
|
||||||
http,
|
http,
|
||||||
credentials_provider: ClientCredentialsProvider::new(cx),
|
credentials_provider: ClientCredentialsProvider::new(cx),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
@ -930,6 +933,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
let credentials = credentials.unwrap();
|
let credentials = credentials.unwrap();
|
||||||
self.set_id(credentials.user_id);
|
self.set_id(credentials.user_id);
|
||||||
|
self.cloud_client
|
||||||
|
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
|
||||||
|
|
||||||
if was_disconnected {
|
if was_disconnected {
|
||||||
self.set_status(Status::Connecting, cx);
|
self.set_status(Status::Connecting, cx);
|
||||||
|
|
21
crates/cloud_api_client/Cargo.toml
Normal file
21
crates/cloud_api_client/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
cloud_api_types.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
http_client.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
76
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
76
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
pub use cloud_api_types::*;
|
||||||
|
use futures::AsyncReadExt as _;
|
||||||
|
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
struct Credentials {
|
||||||
|
user_id: u32,
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CloudApiClient {
|
||||||
|
credentials: RwLock<Option<Credentials>>,
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudApiClient {
|
||||||
|
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||||
|
Self {
|
||||||
|
credentials: RwLock::new(None),
|
||||||
|
http_client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_credentials(&self, user_id: u32, access_token: String) {
|
||||||
|
*self.credentials.write() = Some(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorization_header(&self) -> Result<String> {
|
||||||
|
let guard = self.credentials.read();
|
||||||
|
let credentials = guard
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("No credentials provided"))?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{} {}",
|
||||||
|
credentials.user_id, credentials.access_token
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_authenticated_user(&self) -> Result<AuthenticatedUser> {
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/users/me", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", self.authorization_header()?)
|
||||||
|
.body(AsyncBody::default())?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
let response: GetAuthenticatedUserResponse = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
|
Ok(response.user)
|
||||||
|
}
|
||||||
|
}
|
16
crates/cloud_api_types/Cargo.toml
Normal file
16
crates/cloud_api_types/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_types.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
14
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
14
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetAuthenticatedUserResponse {
|
||||||
|
pub user: AuthenticatedUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AuthenticatedUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub github_login: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
|
@ -236,6 +236,22 @@ impl HttpClientWithUrl {
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a Zed Cloud URL using the given path.
|
||||||
|
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||||
|
let base_url = self.base_url();
|
||||||
|
let base_api_url = match base_url.as_ref() {
|
||||||
|
"https://zed.dev" => "https://cloud.zed.dev",
|
||||||
|
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||||
|
"http://localhost:3000" => "http://localhost:8787",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Url::parse_with_params(
|
||||||
|
&format!("{}{}", base_api_url, path),
|
||||||
|
query,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a Zed LLM URL using the given path.
|
/// Builds a Zed LLM URL using the given path.
|
||||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||||
let base_url = self.base_url();
|
let base_url = self.base_url();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue