feldera-cloud1-client 0.1.3

Telemetry Client for Feldera Cloud1
Documentation
//! License related types exchanged between license server and feldera platform.
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use thiserror::Error as ThisError;
use utoipa::ToSchema;

use crate::source_error;

/// Request to verify a license.
/// Shared type between client and server.
#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct LicenseCheckRequest {
    pub account_id: String,
    pub license_key: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[allow(dead_code)]
pub enum DisplaySchedule {
    /// Display it only once: after dismissal do not show it again
    Once,
    /// Display it again the next session if it is dismissed
    Session,
    /// Display it again after a certain period of time after it is dismissed
    Every { seconds: u64 },
    /// Always display it, do not allow it to be dismissed
    Always,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct LicenseInformation {
    /// Timestamp when the server responded.
    pub current: DateTime<Utc>,
    /// Timestamp at which point the license expires
    pub valid_until: Option<DateTime<Utc>>,
    /// Whether the license is a trial
    pub is_trial: bool,
    /// Optional description of the advantages of extending the license / upgrading from a trial
    pub description_html: String,
    /// URL that navigates the user to extend / upgrade their license
    pub extension_url: Option<String>,
    /// Timestamp from which the user should be reminded of the license expiring soon
    pub remind_starting_at: Option<DateTime<Utc>>,
    /// Suggested frequency of reminding the user about the license expiring soon
    pub remind_schedule: DisplaySchedule,
}

/// Enumeration of the errors when trying to check whether license exists or not.
#[derive(ThisError, Debug, Clone, PartialEq)]
pub enum LicenseRetrievalError {
    #[error("failed to serialize request to JSON due to: {error}")]
    SerializeRequestToJsonFailed { error: String },
    #[error("failed to send request due to: {error}")]
    SendRequestFailed { error: String },
    #[error("license information returned could not be deserialized due to: {error}")]
    JsonDeserializeFailed { error: String },
    #[error("server indicated the service is currently unavailable (503)")]
    ServiceUnavailable,
    #[error("server responded with an unexpected HTTP status code ({0})")]
    UnexpectedResponseStatusCode(StatusCode),
}

#[derive(Debug, Clone, PartialEq)]
pub enum LicenseRetrievalResult {
    /// License with that license key exists and thus the server retrieved information about it.
    /// The information contains whether the license is expired or not.
    Exists(LicenseInformation),

    /// License with that license key does not exist according to the server.
    /// This means the license key is invalid.
    DoesNotExistInvalid,

    /// Unable to check whether the license exists at this moment.
    /// It should be retried in the future.
    Unable(LicenseRetrievalError),
}

/// Verifies the license with the API endpoint.
pub async fn retrieve_license(
    // Cloud API endpoint
    cloud_api_endpoint: &str,
    // License key
    license_key: &str,
) -> (Instant, LicenseRetrievalResult) {
    let endpoint_license_retrieval = format!("{cloud_api_endpoint}/license");
    let client = reqwest::Client::new();
    let result = client
        .get(&endpoint_license_retrieval)
        .header(
            reqwest::header::AUTHORIZATION,
            format!("Bearer {}", license_key),
        )
        .send()
        .await;
    let now = Instant::now();
    let result = match result {
        Ok(response) => {
            let status_code = response.status();
            if status_code == StatusCode::OK {
                match response.json::<LicenseInformation>().await {
                    Ok(license_information) => LicenseRetrievalResult::Exists(license_information),
                    Err(error) => LicenseRetrievalResult::Unable(
                        LicenseRetrievalError::JsonDeserializeFailed {
                            error: error.to_string(),
                        },
                    ),
                }
            } else if status_code == StatusCode::FORBIDDEN {
                LicenseRetrievalResult::DoesNotExistInvalid
            } else if status_code == StatusCode::SERVICE_UNAVAILABLE {
                LicenseRetrievalResult::Unable(LicenseRetrievalError::ServiceUnavailable)
            } else {
                LicenseRetrievalResult::Unable(LicenseRetrievalError::UnexpectedResponseStatusCode(
                    status_code,
                ))
            }
        }
        Err(e) => {
            let source_err = source_error(&e);
            let error = format!("{e}, source: {source_err}");
            LicenseRetrievalResult::Unable(LicenseRetrievalError::SendRequestFailed { error })
        }
    };
    (now, result)
}