Introduce HttpClient trait, use it to fetch avatars in UserStore

* Add a FakeHttpClient for tests
This commit is contained in:
Max Brunsfeld 2021-09-14 12:09:49 -07:00
parent f0019e3725
commit 84d4bb6186
9 changed files with 146 additions and 54 deletions

View File

@ -1,8 +1,11 @@
use crate::geometry::vector::{vec2i, Vector2I};
use image::{Bgra, ImageBuffer};
use std::sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
use std::{
fmt,
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
};
pub struct ImageData {
@ -29,3 +32,12 @@ impl ImageData {
vec2i(width as i32, height as i32)
}
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("id", &self.id)
.field("size", &self.data.dimensions())
.finish()
}
}

View File

@ -1025,6 +1025,7 @@ mod tests {
language::LanguageRegistry,
rpc::{self, Client},
settings,
test::FakeHttpClient,
user::UserStore,
worktree::Worktree,
};
@ -1486,6 +1487,7 @@ mod tests {
// Connect to a server as 2 clients.
let mut server = TestServer::start().await;
let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;

View File

@ -46,7 +46,7 @@ pub struct Channel {
_subscription: rpc::Subscription,
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: u64,
pub body: String,
@ -495,15 +495,17 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count {
#[cfg(test)]
mod tests {
use super::*;
use crate::test::FakeServer;
use crate::test::{FakeHttpClient, FakeServer};
use gpui::TestAppContext;
use surf::http::Response;
#[gpui::test]
async fn test_channel_messages(mut cx: TestAppContext) {
let user_id = 5;
let mut client = Client::new();
let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
let user_store = UserStore::new(client.clone(), cx.background().as_ref());
let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref());
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None));

26
zed/src/http.rs Normal file
View File

@ -0,0 +1,26 @@
pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use std::sync::Arc;
pub use surf::{
http::{Method, Request, Response as ServerResponse},
Response, Url,
};
pub trait HttpClient: Send + Sync {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
}
pub fn client() -> Arc<dyn HttpClient> {
Arc::new(surf::client())
}
impl HttpClient for surf::Client {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
Box::pin(async move {
Ok(self
.send(req)
.await
.map_err(|e| anyhow!("http request failed: {}", e))?)
})
}
}

View File

@ -5,6 +5,7 @@ pub mod editor;
pub mod file_finder;
pub mod fs;
mod fuzzy;
pub mod http;
pub mod language;
pub mod menus;
pub mod project_browser;

View File

@ -13,7 +13,7 @@ use zed::{
channel::ChannelList,
chat_panel, editor, file_finder,
fs::RealFs,
language, menus, rpc, settings, theme_selector,
http, language, menus, rpc, settings, theme_selector,
user::UserStore,
workspace::{self, OpenNew, OpenParams, OpenPaths},
AppState,
@ -37,7 +37,8 @@ fn main() {
app.run(move |cx| {
let rpc = rpc::Client::new();
let user_store = UserStore::new(rpc.clone(), cx.background());
let http = http::client();
let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background());
let app_state = Arc::new(AppState {
languages: languages.clone(),
settings_tx: Arc::new(Mutex::new(settings_tx)),

View File

@ -2,6 +2,7 @@ use crate::{
assets::Assets,
channel::ChannelList,
fs::RealFs,
http::{HttpClient, Request, Response, ServerResponse},
language::LanguageRegistry,
rpc::{self, Client},
settings::{self, ThemeRegistry},
@ -10,11 +11,13 @@ use crate::{
AppState,
};
use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, Future};
use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext};
use parking_lot::Mutex;
use postage::{mpsc, prelude::Stream as _};
use smol::channel;
use std::{
fmt,
marker::PhantomData,
path::{Path, PathBuf},
sync::{
@ -164,7 +167,8 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let languages = Arc::new(LanguageRegistry::new());
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
let rpc = rpc::Client::new();
let user_store = UserStore::new(rpc.clone(), cx.background());
let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
let user_store = UserStore::new(rpc.clone(), http, cx.background());
Arc::new(AppState {
settings_tx: Arc::new(Mutex::new(settings_tx)),
settings,
@ -313,3 +317,33 @@ impl FakeServer {
self.connection_id.lock().expect("not connected")
}
}
pub struct FakeHttpClient {
handler:
Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
}
impl FakeHttpClient {
pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
where
Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
F: 'static + Send + Sync + Fn(Request) -> Fut,
{
Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
})
}
}
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
impl HttpClient for FakeHttpClient {
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
let future = (self.handler)(req);
Box::pin(async move { future.await.map(Into::into) })
}
}

View File

@ -1,22 +1,24 @@
use crate::{
http::{HttpClient, Method, Request, Url},
rpc::{Client, Status},
util::TryFutureExt,
};
use anyhow::{anyhow, Result};
use gpui::{elements::Image, executor, ImageData, Task};
use anyhow::{anyhow, Context, Result};
use futures::future;
use gpui::{executor, ImageData, Task};
use parking_lot::Mutex;
use postage::{prelude::Stream, sink::Sink, watch};
use std::{collections::HashMap, sync::Arc};
use surf::{
http::{Method, Request},
HttpClient, Url,
use postage::{oneshot, prelude::Stream, sink::Sink, watch};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
use zrpc::proto;
#[derive(Debug)]
pub struct User {
id: u64,
github_login: String,
avatar: Option<ImageData>,
pub id: u64,
pub github_login: String,
pub avatar: Option<Arc<ImageData>>,
}
pub struct UserStore {
@ -24,7 +26,7 @@ pub struct UserStore {
current_user: watch::Receiver<Option<Arc<User>>>,
rpc: Arc<Client>,
http: Arc<dyn HttpClient>,
_maintain_current_user: Option<Task<()>>,
_maintain_current_user: Task<()>,
}
impl UserStore {
@ -34,18 +36,18 @@ impl UserStore {
executor: &executor::Background,
) -> Arc<Self> {
let (mut current_user_tx, current_user_rx) = watch::channel();
let mut this = Arc::new(Self {
let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
let this = Arc::new(Self {
users: Default::default(),
current_user: current_user_rx,
rpc: rpc.clone(),
http,
_maintain_current_user: None,
});
let task = {
let this = Arc::downgrade(&this);
executor.spawn(async move {
_maintain_current_user: executor.spawn(async move {
let this = if let Some(this) = this_rx.recv().await {
this
} else {
return;
};
let mut status = rpc.status();
while let Some(status) = status.recv().await {
match status {
@ -63,10 +65,12 @@ impl UserStore {
_ => {}
}
}
})
};
Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task);
}),
});
let weak = Arc::downgrade(&this);
executor
.spawn(async move { this_tx.send(weak).await })
.detach();
this
}
@ -78,8 +82,15 @@ impl UserStore {
if !user_ids.is_empty() {
let response = self.rpc.request(proto::GetUsers { user_ids }).await?;
let new_users = future::join_all(
response
.users
.into_iter()
.map(|user| User::new(user, self.http.as_ref())),
)
.await;
let mut users = self.users.lock();
for user in response.users {
for user in new_users {
users.insert(user.id, Arc::new(user));
}
}
@ -92,20 +103,12 @@ impl UserStore {
return Ok(user);
}
let response = self
.rpc
.request(proto::GetUsers {
user_ids: vec![user_id],
})
.await?;
if let Some(user) = response.users.into_iter().next() {
let user = Arc::new(user);
self.users.lock().insert(user_id, user.clone());
Ok(user)
} else {
Err(anyhow!("server responded with no users"))
}
self.load_users(vec![user_id]).await?;
self.users
.lock()
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
}
pub fn current_user(&self) -> &watch::Receiver<Option<Arc<User>>> {
@ -115,20 +118,25 @@ impl UserStore {
impl User {
async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
let avatar = fetch_avatar(http, &message.avatar_url).await.log_err();
User {
id: message.id,
github_login: message.github_login,
avatar,
avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
}
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let url = Url::parse(url)?;
let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
let request = Request::new(Method::Get, url);
let response = http.send(request).await?;
let bytes = response.body_bytes().await?;
let mut response = http
.send(request)
.await
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
let bytes = response
.body_bytes()
.await
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
let format = image::guess_format(&bytes)?;
let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
Ok(ImageData::new(image))

View File

@ -956,8 +956,14 @@ impl Workspace {
fn render_current_user(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme;
let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() {
todo!()
let avatar = if let Some(avatar) = self
.user_store
.current_user()
.borrow()
.as_ref()
.and_then(|user| user.avatar.clone())
{
Image::new(avatar).boxed()
} else {
Svg::new("icons/signed-out-12.svg")
.with_color(theme.workspace.titlebar.icon_signed_out)