surf
surf copied to clipboard
Example: GitHub SDK
I was talking to @davidbarsky today, and something that would be great to document is how to build an SDK fronted by some sort of login function. This should probably become a proper example (perhaps for GitHub or something?) but here's an initial sketch:
Usage
// intialize the sdk. If there's no local credentials stored, call the callback and get a username + password out
let sdk = Sdk::connect(async || {
let username = dialoguer::Input::new("Username").interact()?;
let password = dialoguer::PasswordInput::new("Password").interact()?;
Ok((username, password))
}).await?;
// If we were able to get the sdk setup, we can now interact with the API!
sdk.upload("/tmp/chashu.png").await?;
Implementation
struct Sdk {
client: surf::Client,
api_token: String,
};
#[derive(serde::DeserializeOwned, serde::Serialize)]
struct Credentials {
api_token: String
}
impl Sdk {
///
/// # Example
/// ```
/// let sdk = Sdk::connect(async || {
/// let username = Input::new("Username").interact()?;
/// let password = PasswordInput::new("Password").interact()?;
/// Ok((username, password))
/// }).await?;
///
/// sdk.upload("/tmp/chashu.png").await?;
/// ```
pub async fn login(login_fn: impl Fn() -> (String, String)) -> Result<Self, LoginError> {
// Try and get the credentials locally
let res = try {
let file = directories::BaseDirs::data_dir().push_str("my_sdk/store.json");
let string = async_std::fs::read_to_string(file).await?;
let creds: Credentials = serde_json::parse(string)?;
creds
};
// Either we had the credentials locally, or we should
match creds {
Ok(creds) => {
// TODO: save api_token locally to disk
Self {
api_token: creds.api_token,
client: surf::Client::new(),
}
}
Err(_) => {
let (username, password) = login_fn?,
let sdk = self::login_with_creds(username, password).await;
// TODO: save sdk.api_token locally to disk
sdk
}
}
}
/// method to login with credentials. Usually called from the callback in
async fn login_with_creds(username: String, password: String) -> Result<Self, LoginError> {
#[derive(serde::Serialize)]
struct Request {
username: String,
password: String,
}
#[derive(serde::DeserializeOwned)]
struct Request {
token: String,
}
let client = surf::Client::new()?;
let creds: Response = client.post("my-app.com/api/login")
.json(Request { username, password })
.recv_json()
.await?;
Ok(Self {
client,
api_token: creds.token
})
}
async fn store_creds(creds: Credentials) -> Result<(), Error> {
let file = directories::BaseDirs::data_dir().push_str("my_sdk/store.json");
mkdirp(file)?;
let buf = serde_json::as_bytes(creds)?;
async_std::fs::write_all(buf).await?;
Ok(())
}
pub async fn upload(&self, file: AsRef<Path>) -> Result<(), Error> {
// upload file to some api using the token and http client
}
}
@yoshuawuyts You would mind if I gave this a try? :)
@AZanellato that'd be fantastic, please do!
@yoshuawuyts I've been giving this a try and got a bit stuck in regards with the type of surf::Client
, like discussed here: https://github.com/rustasync/surf/issues/54
I am trying to do a struct like the one you posted above
struct Sdk {
client: surf::Client,
credentials: VerifiedCredentials
}
But then I need to pass a type to Client
and that got me a bit stuck :/