Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(auth): move api_client to its own module and create it at the app level #26

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/api_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use crate::app::Cli;
use crate::prelude::*;
use gql_client::{Client as GQLClient, ClientConfig};
use nestify::nest;
use serde::{Deserialize, Serialize};

pub struct CodSpeedAPIClient {
pub gql_client: GQLClient,
}

impl From<&Cli> for CodSpeedAPIClient {
fn from(args: &Cli) -> Self {
Self {
gql_client: build_gql_api_client(args.api_url.clone()),
}
}
}

const CODSPEED_GRAPHQL_ENDPOINT: &str = "https://gql.codspeed.io/";

fn build_gql_api_client(api_url: Option<String>) -> GQLClient {
let endpoint = api_url.unwrap_or_else(|| CODSPEED_GRAPHQL_ENDPOINT.to_string());

GQLClient::new_with_config(ClientConfig {
endpoint,
timeout: Some(10),
headers: Default::default(),
proxy: None,
})
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct CreateLoginSessionData {
create_login_session: pub struct CreateLoginSessionPayload {
pub callback_url: String,
pub session_id: String,
}
}
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct ConsumeLoginSessionData {
consume_login_session: pub struct ConsumeLoginSessionPayload {
pub token: Option<String>
}
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConsumeLoginSessionVars {
session_id: String,
}

impl CodSpeedAPIClient {
pub async fn create_login_session(&self) -> Result<CreateLoginSessionPayload> {
let response = self
.gql_client
.query_unwrap::<CreateLoginSessionData>(include_str!("queries/CreateLoginSession.gql"))
.await;
match response {
Ok(response) => Ok(response.create_login_session),
Err(err) => bail!("Failed to create login session: {}", err),
}
}

pub async fn consume_login_session(
&self,
session_id: &str,
) -> Result<ConsumeLoginSessionPayload> {
let response = self
.gql_client
.query_with_vars_unwrap::<ConsumeLoginSessionData, ConsumeLoginSessionVars>(
include_str!("queries/ConsumeLoginSession.gql"),
ConsumeLoginSessionVars {
session_id: session_id.to_string(),
},
)
.await;
match response {
Ok(response) => Ok(response.consume_login_session),
Err(err) => bail!("Failed to use login session: {}", err),
}
}
}
11 changes: 8 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::{auth, prelude::*, run};
use crate::{api_client::CodSpeedAPIClient, auth, prelude::*, run};
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
struct Cli {
pub struct Cli {
/// The URL of the CodSpeed GraphQL API
#[arg(long, env = "CODSPEED_API_URL", global = true, hide = true)]
pub api_url: Option<String>,

#[command(subcommand)]
command: Commands,
}
Expand All @@ -17,10 +21,11 @@ enum Commands {

pub async fn run() -> Result<()> {
let cli = Cli::parse();
let api_client = CodSpeedAPIClient::from(&cli);

match cli.command {
Commands::Run(args) => run::run(args).await?,
Commands::Auth(args) => auth::run(args).await?,
Commands::Auth(args) => auth::run(args, &api_client).await?,
}
Ok(())
}
102 changes: 7 additions & 95 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
use std::time::Duration;

use crate::{config::Config, logger::get_local_logger, prelude::*};
use crate::logger::get_local_logger;
use crate::{api_client::CodSpeedAPIClient, config::CodSpeedConfig, prelude::*};
use clap::{Args, Subcommand};
use gql_client::{Client as GQLClient, ClientConfig};
use nestify::nest;
use serde::{Deserialize, Serialize};
use simplelog::CombinedLogger;
use tokio::time::{sleep, Instant};

#[derive(Debug, Args)]
pub struct AuthArgs {
/// The URL of the CodSpeed GraphQL API
#[arg(long, env = "CODSPEED_API_URL", global = true, hide = true)]
api_url: Option<String>,

#[command(subcommand)]
command: AuthCommands,
}
Expand All @@ -31,100 +25,18 @@ fn init_logger() -> Result<()> {
Ok(())
}

pub async fn run(args: AuthArgs) -> Result<()> {
pub async fn run(args: AuthArgs, api_client: &CodSpeedAPIClient) -> Result<()> {
init_logger()?;
let api_client = CodSpeedAPIClient::from(&args);

match args.command {
AuthCommands::Login => login(api_client).await?,
}
Ok(())
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct CreateLoginSessionData {
create_login_session: struct CreateLoginSessionPayload {
callback_url: String,
session_id: String,
}
}
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct ConsumeLoginSessionData {
consume_login_session: struct ConsumeLoginSessionPayload {
token: Option<String>
}
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConsumeLoginSessionVars {
session_id: String,
}

struct CodSpeedAPIClient {
gql_client: GQLClient,
}

impl From<&AuthArgs> for CodSpeedAPIClient {
fn from(args: &AuthArgs) -> Self {
Self {
gql_client: build_gql_api_client(args.api_url.clone()),
}
}
}

const CODSPEED_GRAPHQL_ENDPOINT: &str = "https://gql.codspeed.io/";

fn build_gql_api_client(api_url: Option<String>) -> GQLClient {
let endpoint = api_url.unwrap_or_else(|| CODSPEED_GRAPHQL_ENDPOINT.to_string());

GQLClient::new_with_config(ClientConfig {
endpoint,
timeout: Some(10),
headers: Default::default(),
proxy: None,
})
}

impl CodSpeedAPIClient {
async fn create_login_session(&self) -> Result<CreateLoginSessionPayload> {
let response = self
.gql_client
.query_unwrap::<CreateLoginSessionData>(include_str!("queries/CreateLoginSession.gql"))
.await;
match response {
Ok(response) => Ok(response.create_login_session),
Err(err) => bail!("Failed to create login session: {}", err),
}
}

async fn consume_login_session(&self, session_id: &str) -> Result<ConsumeLoginSessionPayload> {
let response = self
.gql_client
.query_with_vars_unwrap::<ConsumeLoginSessionData, ConsumeLoginSessionVars>(
include_str!("queries/ConsumeLoginSession.gql"),
ConsumeLoginSessionVars {
session_id: session_id.to_string(),
},
)
.await;
match response {
Ok(response) => Ok(response.consume_login_session),
Err(err) => bail!("Failed to use login session: {}", err),
}
}
}

const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 minutes

async fn login(api_client: CodSpeedAPIClient) -> Result<()> {
async fn login(api_client: &CodSpeedAPIClient) -> Result<()> {
debug!("Login to CodSpeed");
debug!("Creating login session...");
let login_session_payload = api_client.create_login_session().await?;
Expand Down Expand Up @@ -155,9 +67,9 @@ async fn login(api_client: CodSpeedAPIClient) -> Result<()> {
}
debug!("Login completed");

let mut config = Config::load().await?;
config.auth.token = token;
config.persist().await?;
let mut config = CodSpeedConfig::load()?;
config.auth.token = Some(token);
config.persist()?;
debug!("Token saved to configuration file");

info!("Login successful, your are now authenticated on CodSpeed");
Expand Down
26 changes: 13 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{env, path::PathBuf};
use std::{env, fs, path::PathBuf};

use crate::prelude::*;
use nestify::nest;
Expand All @@ -7,9 +7,9 @@ use serde::{Deserialize, Serialize};
nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "kebab-case")]*
pub struct Config {
pub struct CodSpeedConfig {
pub auth: pub struct AuthConfig {
pub token: String,
pub token: Option<String>,
}
}
}
Expand All @@ -27,20 +27,20 @@ fn get_configuration_file_path() -> PathBuf {
config_dir.join("config.yaml")
}

impl Default for Config {
impl Default for CodSpeedConfig {
fn default() -> Self {
Self {
auth: AuthConfig { token: "".into() },
auth: AuthConfig { token: None },
}
}
}

impl Config {
impl CodSpeedConfig {
/// Load the configuration. If it does not exist, store and return a default configuration
pub async fn load() -> Result<Self> {
pub fn load() -> Result<Self> {
let config_path = get_configuration_file_path();

match tokio::fs::read(&config_path).await {
match fs::read(&config_path) {
Ok(config_str) => {
let config = serde_yaml::from_slice(&config_str).context(format!(
"Failed to parse CodSpeed config at {}",
Expand All @@ -51,21 +51,21 @@ impl Config {
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
debug!("Config file not found at {}", config_path.display());
let config = Config::default();
config.persist().await?;
let config = CodSpeedConfig::default();
config.persist()?;
Ok(config)
}
Err(e) => bail!("Failed to load config: {}", e),
}
}

/// Persist changes to the configuration
pub async fn persist(&self) -> Result<()> {
pub fn persist(&self) -> Result<()> {
let config_path = get_configuration_file_path();
tokio::fs::create_dir_all(config_path.parent().unwrap()).await?;
fs::create_dir_all(config_path.parent().unwrap())?;

let config_str = serde_yaml::to_string(self)?;
tokio::fs::write(&config_path, config_str).await?;
fs::write(&config_path, config_str)?;
debug!("Config written to {}", config_path.display());

Ok(())
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod api_client;
mod app;
mod auth;
mod config;
Expand Down
Loading