hypertune 0.6.2

Hypertune SDK for type safe configuration
Documentation
use std::env;

use anyhow::anyhow;
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use thiserror::Error;

use crate::constants;
use crate::context::Context;

pub use crate::constants::DEFAULT_EDGE_BASE_URL;
pub use crate::constants::VERSION;
pub use crate::edge::Language;
pub use crate::evaluate::EvaluationError;
pub use crate::node::Node;
pub use crate::node_props::NodeProps;
pub use crate::node_props::NodePropsError;
pub use crate::node_props::NodePropsType;
pub use crate::primitive_nodes::*;
pub use crate::types::GraphqlQuery;
pub use crate::types::InitQuery;
pub use crate::types::StoredQuery;

#[derive(Error, Debug)]
pub enum HypertuneError {
    #[error("hypertune token environment variable must be set if token is not supplied")]
    MissingToken,

    #[error("failed to deserialize from JSON: {0}")]
    JsonDeserializationError(serde_json::Error),

    #[error("failed to serialize into JSON: {0}")]
    JsonSerializationError(serde_json::Error),

    #[error("failed to initialize context: {0}")]
    ContextInitializationError(anyhow::Error),
}
#[derive(Deserialize, Serialize, Clone)]
pub struct CreateOptions {
    pub branch_name: Option<String>,
    pub init_data_refresh_interval_ms: u64,
    pub logs_flush_interval_ms: u64,
    pub language: Language,
    pub edge_base_url: String,
    pub remote_logging_base_url: String,
}

impl Default for CreateOptions {
    fn default() -> Self {
        Self {
            branch_name: None,
            init_data_refresh_interval_ms: constants::DEFAULT_INIT_DATA_REFRESH_INTERVAL_MS,
            logs_flush_interval_ms: constants::DEFAULT_LOGS_FLUSH_INTERVAL_MS,
            language: Language::Rust,
            edge_base_url: constants::DEFAULT_EDGE_BASE_URL.to_string(),
            remote_logging_base_url: constants::DEFAULT_REMOTE_LOGGING_BASE_URL.to_string(),
        }
    }
}

pub fn create(
    variable_values: impl TryIntoValue,
    fallback_init_data: Option<&str>,
    token: Option<&str>,
    init_query: &InitQuery,
    query: &str,
    options: Option<CreateOptions>,
) -> Result<NodeProps, HypertuneError> {
    let options = match options {
        Some(options) => options,
        None => Default::default(),
    };

    match create_inner(
        variable_values,
        fallback_init_data,
        token,
        init_query,
        query,
        options,
    ) {
        Ok(node) => Ok(node),
        Err(error) => {
            eprintln!("Failed to initialize SDK: {}", error);
            Err(error)
        }
    }
}

fn create_inner(
    variable_values: impl TryIntoValue,
    fallback_init_data: Option<&str>,
    token: Option<&str>,
    query_code: &InitQuery,
    query: &str,
    options: CreateOptions,
) -> Result<NodeProps, HypertuneError> {
    let env_token =
        env::var(constants::HYPERTUNE_TOKEN_ENV_VAR).map_err(|_| HypertuneError::MissingToken);

    let token = match token {
        Some(token) => Ok(token.to_string()),
        None => env_token,
    }?;

    Context::initialize(
        variable_values
            .try_into_value()
            .map_err(HypertuneError::JsonSerializationError)?,
        token,
        query_code.to_owned(),
        serde_json::from_str(query).map_err(HypertuneError::JsonDeserializationError)?,
        options.branch_name,
        options.init_data_refresh_interval_ms,
        options.logs_flush_interval_ms,
        options.edge_base_url,
        options.remote_logging_base_url,
        options.language,
        fallback_init_data
            .map(serde_json::from_str)
            .transpose()
            .map_err(HypertuneError::JsonDeserializationError)?,
    )
    .map_err(HypertuneError::ContextInitializationError)
}

pub trait NodeMethods {
    fn get_field(&self, field: &str, args: impl TryIntoValue) -> NodeProps;

    fn evaluate(&self) -> Result<Value, EvaluationError>;

    fn log_unexpected_type_error(&self);

    fn log_unexpected_value_error(&self, value: Result<Value, EvaluationError>);
}

fn get_field_helper(node: &Node, field: &str, args: impl TryIntoValue) -> Result<NodeProps> {
    match node.get_field(
        field,
        args.try_into_value()
            .map_err(HypertuneError::JsonSerializationError)?,
    ) {
        Ok(props) => Ok(props),
        Err(NodePropsError::GetField(props)) => Ok(props),
    }
}

impl<T> NodeMethods for T
where
    T: GetNode,
{
    fn get_field(&self, field: &str, args: impl TryIntoValue) -> NodeProps {
        let node = self.get_node();
        match get_field_helper(node, field, args) {
            Ok(props) => props,
            Err(_) => node.get_error_node_props(),
        }
    }

    fn evaluate(&self) -> Result<Value, EvaluationError> {
        self.get_node().evaluate()
    }

    fn log_unexpected_type_error(&self) {
        self.get_node().log_unexpected_type_error()
    }

    fn log_unexpected_value_error(&self, value: Result<Value, EvaluationError>) {
        self.get_node().log_unexpected_value_error(value)
    }
}

pub trait TryIntoValue {
    fn try_into_value(self) -> Result<Value, serde_json::Error>;
}

impl<T> TryIntoValue for T
where
    T: serde::ser::Serialize,
{
    fn try_into_value(self) -> Result<Value, serde_json::Error> {
        serde_json::to_value(self)
    }
}

pub trait TryFromValue
where
    Self: Sized,
{
    fn try_from_value(value: Option<Value>) -> Result<Self>;
}

impl<T> TryFromValue for T
where
    T: serde::de::DeserializeOwned,
{
    fn try_from_value(value: Option<Value>) -> Result<Self> {
        match value {
            Some(value) => Ok(serde_json::from_value(value)?),
            None => Err(anyhow!("Empty value supplied")),
        }
    }
}