cloud-pubsub 0.9.0

Google Cloud PubSub Client
Documentation
use crate::error;
use crate::subscription::Subscription;
use crate::topic::Topic;
use goauth::auth::JwtClaims;
use goauth::scopes::Scope;
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
use smpl_jwt::Jwt;
use std::fs;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::task;
use tokio::time;

type HyperClient = Arc<hyper::Client<HttpsConnector<HttpConnector>, hyper::Body>>;

pub struct State {
    token: Option<goauth::auth::Token>,
    credentials_string: String,
    project: Option<String>,
    hyper_client: HyperClient,
    running: Arc<AtomicBool>,
}

impl State {
    pub fn token_type(&self) -> &str {
        self.token.as_ref().unwrap().token_type()
    }

    pub fn access_token(&self) -> &str {
        self.token.as_ref().unwrap().access_token()
    }

    pub fn project(&self) -> &str {
        &(self.project.as_ref().expect("Google Cloud Project has not been set. If it is not in your credential file, call set_project to set it manually."))
    }
}

pub struct Client(Arc<RwLock<State>>);

impl Clone for Client {
    fn clone(&self) -> Self {
        Client(self.0.clone())
    }
}

impl Client {
    pub async fn from_string(credentials_string: String) -> Result<Self, error::Error> {
        let mut client = Client(Arc::new(RwLock::new(State {
            token: None,
            credentials_string,
            project: None,
            hyper_client: setup_hyper(),
            running: Arc::new(AtomicBool::new(true)),
        })));

        match client.refresh_token().await {
            Ok(_) => Ok(client),
            Err(e) => Err(e),
        }
    }

    pub async fn new(credentials_path: String) -> Result<Self, error::Error> {
        let credentials_string = fs::read_to_string(credentials_path).unwrap();
        Self::from_string(credentials_string).await
    }

    pub fn subscribe(&self, name: String) -> Subscription {
        Subscription {
            client: Some(self.clone()),
            name: format!("projects/{}/subscriptions/{}", self.project(), name),
            topic: None,
        }
    }

    pub fn set_project(&mut self, project: String) {
        self.0.write().unwrap().project = Some(project);
    }

    pub fn project(&self) -> String {
        self.0.read().unwrap().project().to_string()
    }

    pub fn topic(&self, name: String) -> Topic {
        Topic {
            client: Some(Client(self.0.clone())),
            name: format!("projects/{}/topics/{}", self.project(), name),
        }
    }

    pub fn is_running(&self) -> bool {
        self.0.read().unwrap().running.load(Ordering::SeqCst)
    }

    pub fn stop(&self) {
        self.0
            .write()
            .unwrap()
            .running
            .store(false, Ordering::SeqCst)
    }

    pub fn spawn_token_renew(&self, interval: Duration) {
        let mut client = self.clone();
        let c = self.clone();
        let renew_token_task = async move {
            let mut int = time::interval(interval);
            loop {
                if c.is_running() {
                    int.tick().await;
                    log::debug!("Renewing pubsub token");
                    if let Err(e) = client.refresh_token().await {
                        log::error!("Failed to update token: {}", e);
                    }
                }
            }
        };

        task::spawn(renew_token_task);
    }

    pub async fn refresh_token(&mut self) -> Result<(), error::Error> {
        match self.get_token().await {
            Ok(token) => {
                self.0.write().unwrap().token = Some(token);
                Ok(())
            }
            Err(e) => Err(error::Error::from(e)),
        }
    }

    async fn get_token(&mut self) -> Result<goauth::auth::Token, goauth::GoErr> {
        let credentials =
            goauth::credentials::Credentials::from_str(&self.0.read().unwrap().credentials_string)
                .unwrap();

        self.set_project(credentials.project());

        let claims = JwtClaims::new(
            credentials.iss(),
            &Scope::PubSub,
            credentials.token_uri(),
            None,
            None,
        );
        let jwt = Jwt::new(claims, credentials.rsa_key().unwrap(), None);
        goauth::get_token(&jwt, &credentials).await
    }

    pub(crate) fn request<T: Into<hyper::Body>>(
        &self,
        method: hyper::Method,
        data: T,
    ) -> hyper::Request<hyper::Body>
    where
        hyper::Body: std::convert::From<T>,
    {
        let mut req = hyper::Request::new(hyper::Body::from(data));
        *req.method_mut() = method;
        req.headers_mut().insert(
            hyper::header::CONTENT_TYPE,
            hyper::header::HeaderValue::from_static("application/json"),
        );
        let readable = self.0.read().unwrap();
        req.headers_mut().insert(
            hyper::header::AUTHORIZATION,
            hyper::header::HeaderValue::from_str(&format!(
                "{} {}",
                readable.token_type(),
                readable.access_token()
            ))
            .unwrap(),
        );
        req
    }

    pub fn hyper_client(&self) -> HyperClient {
        self.0.read().unwrap().hyper_client.clone()
    }
}

fn setup_hyper() -> HyperClient {
    let https = HttpsConnector::new();
    Arc::new(hyper::Client::builder().build::<_, hyper::Body>(https))
}