use std::collections::BTreeMap;
use std::fmt;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::{Error, Result};
use crate::release_access::{ReleaseAccess, lenient_release_access};
use crate::sub_product::{SubProduct, lenient_sub_products};
use crate::time::serde_iso8601;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ActivationMethod {
#[default]
#[serde(alias = "online")]
Online,
#[serde(alias = "offline")]
Offline,
}
impl ActivationMethod {
pub fn as_str(self) -> &'static str {
match self {
Self::Online => "Online",
Self::Offline => "Offline",
}
}
pub(crate) fn parse(value: &str) -> Result<Self> {
match value {
"Online" | "online" => Ok(Self::Online),
"Offline" | "offline" => Ok(Self::Offline),
other => Err(Error::LicenseInvalid(format!("Unknown activation method: {other}"))),
}
}
}
impl fmt::Display for ActivationMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Platform {
Unknown,
Windows,
Linux,
Mac,
}
impl Platform {
pub const fn current() -> Self {
if cfg!(target_os = "windows") {
Self::Windows
} else if cfg!(target_os = "macos") {
Self::Mac
} else if cfg!(target_os = "linux") {
Self::Linux
} else {
Self::Unknown
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Windows => "Windows",
Self::Linux => "Linux",
Self::Mac => "Mac",
Self::Unknown => "Unknown",
}
}
}
impl Default for Platform {
fn default() -> Self {
Self::current()
}
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn empty_object() -> Value {
Value::Object(Default::default())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Product {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_release_version: Option<String>,
#[serde(default = "empty_object")]
pub properties: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
#[serde(default = "empty_object")]
pub properties: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct License {
pub id: String,
pub activation_id: String,
pub trial: bool,
#[serde(rename = "activationMethod")]
pub method: ActivationMethod,
#[serde(rename = "product")]
pub licensed_product: Product,
pub issued_to: User,
#[serde(with = "serde_iso8601")]
pub issued_at: SystemTime,
#[serde(default, with = "serde_iso8601::option", skip_serializing_if = "Option::is_none")]
pub expires_at: Option<SystemTime>,
#[serde(with = "serde_iso8601")]
pub validated_at: SystemTime,
#[serde(default)]
pub owned_sub_product_ids: Vec<String>,
#[serde(default, deserialize_with = "lenient_sub_products")]
pub sub_products: Vec<SubProduct>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscription_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seat_count: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seats_used: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "lenient_release_access")]
pub release_access: Option<ReleaseAccess>,
#[serde(default = "empty_object")]
pub properties: Value,
pub token: String,
}
impl License {
pub fn is_expired(&self) -> bool {
self.expires_at.is_some_and(|expires| expires < SystemTime::now())
}
pub fn owns_sub_product(&self, sub_product_id: &str) -> bool {
self.owned_sub_product_ids.iter().any(|id| id == sub_product_id)
}
pub fn find_sub_product(&self, sub_product_id: &str) -> Option<&SubProduct> {
let own_id = sub_product_id
.strip_prefix(self.licensed_product.id.as_str())
.and_then(|rest| rest.strip_prefix('.'))
.filter(|rest| !rest.is_empty())
.unwrap_or(sub_product_id);
self.sub_products.iter().find(|entry| entry.id == own_id)
}
pub fn has_sub_product(&self, sub_product_id: &str) -> bool {
self.has_sub_product_at(sub_product_id, SystemTime::now())
}
pub fn has_sub_product_at(&self, sub_product_id: &str, now: SystemTime) -> bool {
self.find_sub_product(sub_product_id)
.is_some_and(|entry| entry.active && entry.expires_at.is_none_or(|expires| expires > now))
}
pub fn covers_version(&self, version: &str) -> bool {
self.covers_version_at(version, SystemTime::now())
}
pub fn covers_version_at(&self, version: &str, now: SystemTime) -> bool {
self.release_access.as_ref().is_none_or(|access| access.covers_at(version, now))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivationRequest {
pub id: String,
#[serde(rename = "request")]
pub request_url: String,
#[serde(rename = "browser")]
pub browser_url: String,
#[serde(rename = "activationMethod", default)]
pub method: ActivationMethod,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct LicensingOptions {
pub endpoint: String,
pub product_id: String,
pub public_key: String,
pub account_id: Option<String>,
pub platform: Platform,
pub application_version: Option<String>,
pub enforce_release_access: bool,
pub client_info: Option<String>,
pub metadata: BTreeMap<String, String>,
pub auto_activate_sub_products: bool,
pub http_connect_timeout: Duration,
pub http_request_timeout: Duration,
pub online_validation_grace_period: Duration,
pub online_validation_min_interval: Duration,
}
impl LicensingOptions {
pub fn new(endpoint: impl Into<String>, product_id: impl Into<String>, public_key: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
product_id: product_id.into(),
public_key: public_key.into(),
account_id: None,
platform: Platform::current(),
application_version: None,
enforce_release_access: true,
client_info: None,
metadata: BTreeMap::new(),
auto_activate_sub_products: true,
http_connect_timeout: Duration::from_secs(10),
http_request_timeout: Duration::from_secs(30),
online_validation_grace_period: Duration::from_secs(7 * 24 * 60 * 60),
online_validation_min_interval: Duration::from_secs(5 * 60),
}
}
pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
pub fn application_version(mut self, version: impl Into<String>) -> Self {
self.application_version = Some(version.into());
self
}
pub fn enforce_release_access(mut self, enforce: bool) -> Self {
self.enforce_release_access = enforce;
self
}
pub fn auto_activate_sub_products(mut self, auto_activate: bool) -> Self {
self.auto_activate_sub_products = auto_activate;
self
}
pub fn client_info(mut self, client_info: impl Into<String>) -> Self {
self.client_info = Some(client_info.into());
self
}
pub fn platform(mut self, platform: Platform) -> Self {
self.platform = platform;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn http_timeouts(mut self, connect: Duration, request: Duration) -> Self {
self.http_connect_timeout = connect;
self.http_request_timeout = request;
self
}
pub fn online_validation_grace_period(mut self, grace: Duration) -> Self {
self.online_validation_grace_period = grace;
self
}
pub fn online_validation_min_interval(mut self, interval: Duration) -> Self {
self.online_validation_min_interval = interval;
self
}
}