roctokit 0.15.0

Github v3 Client interfaces
Documentation
use super::{GitHubRequest, GitHubResponseExt};
use crate::auth::Auth;
use base64::{prelude::BASE64_STANDARD, Engine};

use chrono::{DateTime, Duration, Utc};
use js_sys::{Object, Promise, Reflect};
use log::debug;
use serde::{ser, Deserialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, Response, ServiceWorkerGlobalScope, Window};

trait WasmScope {
    fn fetch(&self, request: &Request) -> Promise;
}

impl WasmScope for Window {
    fn fetch(&self, request: &Request) -> Promise {
        self.fetch_with_request(request)
    }
}

impl WasmScope for ServiceWorkerGlobalScope {
    fn fetch(&self, request: &Request) -> Promise {
        self.fetch_with_request(request)
    }
}

fn scope() -> Result<Box<dyn WasmScope>, AdapterError> {
    let global = js_sys::global();
    if Reflect::has(&global, &JsValue::from_str("document"))? {
        debug!("Found document, using Window for scope");
        Ok(Box::new(global.dyn_into::<Window>()?))
    } else {
        debug!("Using ServiceWorkerGlobalScope for scope");
        Ok(Box::new(global.dyn_into::<ServiceWorkerGlobalScope>()?))
    }
}

#[derive(thiserror::Error, Debug)]
pub enum AdapterError {
    #[error(transparent)]
    Value(JsValueError),
    #[error(transparent)]
    Object(ObjectError),
    #[error(transparent)]
    Serde(#[from] serde_json::Error),
    #[error(transparent)]
    IOError(#[from] std::io::Error),
    #[error("Wasm adapter only has async fetch implemented")]
    UnimplementedSync,
}

impl From<AdapterError> for crate::adapters::AdapterError {
    fn from(err: AdapterError) -> Self {
        Self::Client {
            description: err.to_string(),
            source: Some(Box::new(err)),
        }
    }
}

#[derive(thiserror::Error, Debug)]
#[error("{msg}")]
pub struct JsValueError {
    msg: String,
    origin: JsValue,
}

#[derive(thiserror::Error, Debug)]
#[error("{msg}")]
pub struct ObjectError {
    msg: String,
    origin: Object,
}

impl super::Client for Client {
    type Req = web_sys::Request;
    type Body = JsValue;
    type Err = AdapterError where crate::adapters::AdapterError: From<Self::Err>;

    fn new(auth: &Auth) -> Result<Self, Self::Err> {
        Ok(Self {
            auth: auth.to_owned(),
            scope: scope()?,
        })
    }

    fn fetch(&self, _req: Self::Req) -> Result<impl GitHubResponseExt, Self::Err> {
        Err::<GitHubResponse, _>(AdapterError::UnimplementedSync)
    }

    async fn fetch_async(&self, request: Self::Req) -> Result<impl GitHubResponseExt, Self::Err> {
        let resp_value = JsFuture::from(self.scope.fetch(&request)).await?;

        debug!("Response: {:?}", &resp_value);

        let resp: Response = resp_value.dyn_into()?;
        let json: JsValue = JsFuture::from(resp.json()?).await?;

        debug!("Body: {:?}", &json);

        Ok(GitHubResponse { json, resp })
    }

fn build(&self, req: GitHubRequest<Self::Body>) -> Result<Self::Req, Self::Err> {
        let opts = RequestInit::new();
        opts.set_method(&req.method);
        if let Some(body) = req.body {
            debug!("Adding request body: {:?}", &body);

            opts.set_body(&js_sys::JSON::stringify(&body)?.into());
        }

        let request = Request::new_with_str_and_init(&req.uri, &opts)?;
        let headers = request.headers();
        headers.set("Accept", "application/vnd.github.v3+json")?;
        headers.set("Content-Type", "application/json")?;
        headers.set("User-Agent", "roctogen")?;

        for header in req.headers.iter() {
            headers.set(header.0, header.1)?;
        }

        match &self.auth {
            Auth::Basic { user, pass } => {
                let creds = format!("{}:{}", user, pass);
                headers.set(
                    "Authorization",
                    &format!("Basic {}", BASE64_STANDARD.encode(creds.as_bytes())),
                )?;
            }
            Auth::Token(token) => headers.set("Authorization", &format!("token {}", token))?,
            Auth::Bearer(bearer) => headers.set("Authorization", &format!("Bearer {}", bearer))?,
            Auth::None => (),
        }

        debug!("Built request object: {:?}", &request);

        Ok(request)
    }

    fn from_json<E: ser::Serialize>(model: E) -> Result<Self::Body, Self::Err> {
        Ok(JsValue::from_serde(&model)?)
        }
}

pub struct Client {
    auth: Auth,
    scope: Box<dyn WasmScope>,
}

impl From<JsValue> for AdapterError {
    fn from(js_value: JsValue) -> Self {
        AdapterError::Value(JsValueError {
            msg: format!("{:#?}", js_value),
            origin: js_value,
        })
    }
}

impl From<Object> for AdapterError {
    fn from(js_obj: Object) -> Self {
        AdapterError::Object(ObjectError {
            msg: format!("{:#?}", js_obj),
            origin: js_obj,
        })
    }
}

pub(crate) struct GitHubResponse {
    pub json: JsValue,
    pub resp: Response,
}

impl GitHubResponseExt for GitHubResponse {
    fn is_success(&self) -> bool {
        self.resp.ok()
    }

    fn status_code(&self) -> u16 {
        self.resp.status()
    }
    fn to_json<E: for<'de> Deserialize<'de> + std::fmt::Debug>(self) -> Result<E, serde_json::Error> {
        unimplemented!("Reqwest adapter only has async json conversion implemented");
    }

    async fn to_json_async<E: for<'de> Deserialize<'de> + Unpin + std::fmt::Debug>(
        self,
    ) -> Result<E, serde_json::Error> {
        Ok(self.json.into_serde()?)
    }
}