hypertune 0.6.2

Hypertune SDK for type safe configuration
Documentation
use std::collections::HashMap;

use anyhow::anyhow;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value;

use crate::constants;
use crate::expression::Expression;
use crate::split::CommitConfig;
use crate::split::SplitMap;
use crate::types::InitQuery;
use crate::types::Message;

#[derive(Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum Language {
    Python,
    Rust,
    Go,
}

#[derive(Deserialize)]
pub struct CodegenFile {
    pub name: String,
    pub content: String,
}
#[derive(Deserialize)]
pub struct CodegenResponse {
    pub files: Vec<CodegenFile>,
    pub messages: Vec<Message>,
}

#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InitializationData {
    pub reduced_expression: Expression,
    pub hash: String,
    pub commit_config: CommitConfig,
    pub splits: SplitMap,
    pub commit_id: u64,
}
pub struct HashResponse {
    pub commit_id: u64,
    pub hash: String,
}

pub async fn codegen_request(
    token: &str,
    branch_name: Option<String>,
    language: Language,
    sdk_version: String,
    query: Option<&str>,
    include_token: bool,
    include_fallback: bool,
    base_url: &str,
) -> Result<CodegenResponse, reqwest::Error> {
    let body = json!({
        "query": query,
        "includeToken": include_token,
        "includeFallback": include_fallback,
        "sdkType": language,
        "sdkVersion": sdk_version,
        "language": language,
    })
    .to_string();
    let mut params = HashMap::new();
    params.insert("token", token);
    params.insert("body", &body);
    if let Some(ref branch_name) = branch_name {
        params.insert("branch", branch_name.as_str());
    }

    let client = reqwest::Client::new();
    client
        .get(format!("{}/codegen", base_url))
        .query(&params)
        .send()
        .await?
        .error_for_status()?
        .json()
        .await
}

pub async fn init_request(
    token: &str,
    branch_name: Option<String>,
    query: &InitQuery,
    variables: &Value,
    language: Language,
    base_url: &str,
) -> Result<InitializationData, reqwest::Error> {
    let body = &json!({
        "query": query,
        "variables": variables,
        "sdkType": language,
        "sdkVersion": constants::VERSION,
    })
    .to_string();
    let mut params = HashMap::new();
    params.insert("token", token);
    params.insert("body", body);
    if let Some(ref branch_name) = branch_name {
        params.insert("branch", branch_name.as_str());
    }

    let client = reqwest::Client::new();
    client
        .get(format!("{}/init", base_url))
        .query(&params)
        .send()
        .await?
        .error_for_status()?
        .json()
        .await
}

pub async fn hash_request(
    token: &str,
    branch_name: Option<String>,
    query: &InitQuery,
    variables: &Value,
    language: Language,
    base_url: &str,
) -> Result<HashResponse, anyhow::Error> {
    let body = &json!({
        "query": query,
        "variables": variables,
        "sdkType": language,
        "sdkVersion": constants::VERSION,
    })
    .to_string();
    let mut params = HashMap::new();
    params.insert("token", token);
    params.insert("body", body);
    if let Some(ref branch_name) = branch_name {
        params.insert("branch", branch_name.as_str());
    }

    let client = reqwest::Client::new();
    let text = client
        .get(format!("{}/hash", base_url))
        .query(&params)
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;

    if let Some((raw_commit_id, hash)) = text.split_once('_') {
        let commit_id = raw_commit_id.parse::<u64>()?;
        Ok(HashResponse {
            commit_id,
            hash: hash.to_string(),
        })
    } else {
        Err(anyhow!("Invalid hash response: {}", text))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::expression::BooleanExpression;
    use crate::expression::Logs;
    use crate::expression::ObjectExpression;
    use std::collections::HashMap;

    #[test]
    fn test_deserialization() {
        let json = r#"
        {
            "commitId": 3535,
            "hash": "7719744257597328",
            "reducedExpression": {
              "id": "ih422aM1N7w5u1pZHjiHj",
              "type": "ObjectExpression",
              "fields": {
                "root": {
                  "id": "gZDqtndJoiI8RQ7q4CuCo",
                  "logs": {
                    "evaluations": {
                      "gy-OTD4_b0zH2XOmaXctc": 1
                    }
                  },
                  "type": "ObjectExpression",
                  "fields": {
                    "simpleFlag": {
                      "id": "3Py1edtYsW_dEqdwEewak",
                      "logs": {
                        "evaluations": {
                          "WO2MDkDuq0TMU5_MvKSe6": 1
                        }
                      },
                      "type": "BooleanExpression",
                      "value": false,
                      "valueType": {
                        "type": "BooleanValueType"
                      }
                    }
                  }, 
                  "valueType": {
                    "type": "ObjectValueType",
                    "objectTypeName": "Root"
                  },
                  "objectTypeName": "Root"
                }
              },
              "metadata": {
                "permissions": {
                  "user": {},
                  "group": {
                    "team": {
                      "write": "allow"
                    }
                  }
                }
              },
              "valueType": {
                "type": "ObjectValueType",
                "objectTypeName": "Query"
              },
              "objectTypeName": "Query"
            },
            "splits": {},
            "commitConfig": {
              "splitConfig": {}
            }
          }
"#;

        let deserialized: InitializationData = serde_json::from_str(json).unwrap();

        let expected = InitializationData {
            commit_id: 3535,
            hash: "7719744257597328".to_string(),
            commit_config: CommitConfig::new(),
            splits: SplitMap::new(),
            reduced_expression: Expression::Object(ObjectExpression {
                id: "ih422aM1N7w5u1pZHjiHj".to_string(),
                object_type_name: "Query".to_string(),
                logs: None,
                is_transient: false,
                fields: HashMap::from([(
                    "root".to_string(),
                    Expression::Object(ObjectExpression {
                        id: "gZDqtndJoiI8RQ7q4CuCo".to_string(),
                        object_type_name: "Root".to_string(),
                        is_transient: false,
                        logs: Some(Logs {
                            evaluations: Some(HashMap::from([(
                                "gy-OTD4_b0zH2XOmaXctc".to_string(),
                                1,
                            )])),
                            event_list: None,
                            exposure_list: None,
                        }),
                        fields: HashMap::from([(
                            "simpleFlag".to_string(),
                            Expression::Boolean(BooleanExpression {
                                id: "3Py1edtYsW_dEqdwEewak".to_string(),
                                value: false,
                                is_transient: false,
                                logs: Some(Logs {
                                    evaluations: Some(HashMap::from([(
                                        "WO2MDkDuq0TMU5_MvKSe6".to_string(),
                                        1,
                                    )])),
                                    event_list: None,
                                    exposure_list: None,
                                }),
                            }),
                        )]),
                    }),
                )]),
            }),
        };

        assert_eq!(deserialized, expected)
    }
}