use crate::Grid;
use crate::{
add_backslash_if_necessary, has_valid_path_segments, http_response_to_grid,
new_auth_token,
};
use serde_json::json;
use thiserror::Error;
use url::Url;
pub async fn eval(
client: &reqwest::Client,
project_api_url: &str,
username: &str,
password: &str,
axon_expr: &str,
auth_token: Option<&str>,
) -> Result<EvalOutput, EvalError> {
let project_api_url = Url::parse(project_api_url)?;
let project_api_url = add_backslash_if_necessary(project_api_url);
if project_api_url.cannot_be_a_base() {
let url_err_msg = "the project API URL must be a valid base URL";
return Err(EvalError::UrlFormat(url_err_msg.to_owned()));
}
if !has_valid_path_segments(&project_api_url) {
let url_err_msg = "URL must be formatted similarly to http://www.test.com/api/project/";
return Err(EvalError::UrlFormat(url_err_msg.to_owned()));
}
let eval_url = project_api_url
.join("eval")
.expect("since url ends with '/' this should never fail");
let mut was_new_token_obtained = false;
let auth_token = match auth_token {
Some(token) => token.to_owned(),
None => {
was_new_token_obtained = true;
new_auth_token(&project_api_url, client, username, password).await?
}
};
let row = json!({ "expr": axon_expr });
let req_grid = Grid::new_internal(vec![row]);
let req_with_token = |token: &str| {
client
.post(eval_url.clone())
.header("Accept", "application/json")
.header("Authorization", format!("BEARER authToken={}", token))
.header("Content-Type", "application/json")
.body(req_grid.to_json_string())
};
let res = req_with_token(&auth_token).send().await?;
if res.status() == reqwest::StatusCode::FORBIDDEN {
let auth_token =
new_auth_token(&project_api_url, client, username, password)
.await?;
let retry_res = req_with_token(&auth_token).send().await?;
let grid: Result<Grid, EvalError> = http_response_to_grid(retry_res)
.await
.map_err(|err| err.into());
Ok(EvalOutput::new(grid?, Some(auth_token)))
} else {
let grid: Result<Grid, EvalError> =
http_response_to_grid(res).await.map_err(|err| err.into());
if was_new_token_obtained {
Ok(EvalOutput::new(grid?, Some(auth_token)))
} else {
Ok(EvalOutput::new(grid?, None))
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct EvalOutput {
grid: Grid,
new_auth_token: Option<String>,
}
impl EvalOutput {
fn new(grid: Grid, new_auth_token: Option<String>) -> Self {
Self {
grid,
new_auth_token,
}
}
pub fn grid(&self) -> &Grid {
&self.grid
}
pub fn into_grid(self) -> Grid {
self.grid
}
pub fn has_new_auth_token(&self) -> bool {
self.new_auth_token.is_some()
}
pub fn new_auth_token(&self) -> Option<&str> {
match &self.new_auth_token {
Some(token) => Some(token),
None => None,
}
}
}
#[derive(Debug, Error)]
pub enum EvalError {
#[error("Could not parse a URL: {0}")]
UrlParse(#[from] url::ParseError),
#[error("URL is not formatted for the SkySpark API: {0}")]
UrlFormat(String),
#[error("Authentication error: {0}")]
Auth(#[from] crate::auth::AuthError),
#[error("Server returned an error grid")]
Grid {
err_grid: Grid,
},
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Could not parse JSON as a Haystack grid")]
ParseJsonGrid(#[from] crate::grid::ParseJsonGridError),
#[error("Not a valid time zone: {err_time_zone}")]
TimeZone {
err_time_zone: String,
},
}
impl std::convert::From<crate::Error> for EvalError {
fn from(error: crate::Error) -> Self {
match error {
crate::Error::Grid { err_grid } => Self::Grid { err_grid },
crate::Error::Http { err } => Self::Http(err),
crate::Error::ParseJsonGrid(err) => Self::ParseJsonGrid(err),
crate::Error::TimeZone { err_time_zone } => {
Self::TimeZone { err_time_zone }
}
crate::Error::UpdateAuthToken(_) => unreachable!(), }
}
}
impl EvalError {
pub fn is_grid(&self) -> bool {
matches!(self, Self::Grid { .. })
}
pub fn grid(&self) -> Option<&Grid> {
match self {
Self::Grid { err_grid } => Some(err_grid),
_ => None,
}
}
pub fn into_grid(self) -> Option<Grid> {
match self {
Self::Grid { err_grid } => Some(err_grid),
_ => None,
}
}
}
#[cfg(test)]
mod test {
use super::eval;
use super::{EvalError, EvalOutput};
use crate::ValueExt;
fn project_api_url() -> String {
std::env::var("RAYSTACK_SKYSPARK_PROJECT_API_URL").unwrap()
}
fn username() -> String {
std::env::var("RAYSTACK_SKYSPARK_USERNAME").unwrap()
}
fn password() -> String {
std::env::var("RAYSTACK_SKYSPARK_PASSWORD").unwrap()
}
async fn eval_expr(
axon_expr: &str,
token: Option<&str>,
) -> Result<EvalOutput, EvalError> {
let client = reqwest::Client::new();
eval(
&client,
&project_api_url(),
&username(),
&password(),
axon_expr,
token,
)
.await
}
#[tokio::test]
async fn eval_works_with_no_token() {
let output = eval_expr("readAll(site)", None).await.unwrap();
assert!(output.has_new_auth_token());
let grid = output.into_grid();
assert!(grid.size() > 1);
assert!(grid.rows()[0]["site"].is_hs_marker());
}
#[tokio::test]
async fn eval_works_with_bad_token() {
let output = eval_expr("readAll(site)", Some("thistokenisnotvalid"))
.await
.unwrap();
assert!(output.has_new_auth_token());
let grid = output.into_grid();
assert!(grid.size() > 1);
assert!(grid.rows()[0]["site"].is_hs_marker());
}
#[tokio::test]
async fn eval_works_with_good_token() {
let output_for_token = eval_expr("readAll(site)", None).await.unwrap();
let valid_token = output_for_token.new_auth_token().unwrap();
let output =
eval_expr("readAll(site)", Some(valid_token)).await.unwrap();
assert_eq!(output.has_new_auth_token(), false);
let grid = output.into_grid();
assert!(grid.size() > 1);
assert!(grid.rows()[0]["site"].is_hs_marker());
}
}