http-rs / surf

Fast and friendly HTTP client framework for async Rust
https://docs.rs/surf
Apache License 2.0
1.45k stars 119 forks source link

Example: GitHub SDK #10

Open yoshuawuyts opened 5 years ago

yoshuawuyts commented 5 years ago

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
    }
}
AZanellato commented 4 years ago

@yoshuawuyts You would mind if I gave this a try? :)

yoshuawuyts commented 4 years ago

@AZanellato that'd be fantastic, please do!

AZanellato commented 4 years ago

@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 :/