use crate::client::Client;
use crate::errors::Error;
use crate::insert_optional;
use crate::token::{token_request_attributes, CreateTokenRequest, Token, TokenResponse};
use crate::KeygenResponseData;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DistributionStrategy {
Open,
Closed,
Licensed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Platform {
Windows,
MacOs,
Linux,
Darwin,
Android,
Ios,
Web,
Other(String),
}
impl Serialize for Platform {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Platform::Windows => serializer.serialize_str("windows"),
Platform::MacOs => serializer.serialize_str("macOS"),
Platform::Linux => serializer.serialize_str("linux"),
Platform::Darwin => serializer.serialize_str("darwin"),
Platform::Android => serializer.serialize_str("android"),
Platform::Ios => serializer.serialize_str("iOS"),
Platform::Web => serializer.serialize_str("web"),
Platform::Other(s) => serializer.serialize_str(s),
}
}
}
impl<'de> Deserialize<'de> for Platform {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"windows" => Platform::Windows,
"macOS" => Platform::MacOs,
"linux" => Platform::Linux,
"darwin" => Platform::Darwin,
"android" => Platform::Android,
"iOS" => Platform::Ios,
"web" => Platform::Web,
_ => Platform::Other(s),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductAttributes {
pub name: String,
pub code: Option<String>,
#[serde(rename = "distributionStrategy")]
pub distribution_strategy: Option<DistributionStrategy>,
pub url: Option<String>,
pub platforms: Option<Vec<Platform>>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub created: String,
pub updated: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProductResponse {
pub data: KeygenResponseData<ProductAttributes>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProductsResponse {
pub data: Vec<KeygenResponseData<ProductAttributes>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateProductRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(rename = "distributionStrategy")]
pub distribution_strategy: Option<DistributionStrategy>,
pub url: Option<String>,
pub platforms: Option<Vec<Platform>>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListProductsOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(rename = "page[size]", skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
#[serde(rename = "page[number]", skip_serializing_if = "Option::is_none")]
pub page_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateProductRequest {
pub name: Option<String>,
pub code: Option<String>,
#[serde(rename = "distributionStrategy")]
pub distribution_strategy: Option<DistributionStrategy>,
pub url: Option<String>,
pub platforms: Option<Vec<Platform>>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone)]
pub struct Product {
pub id: String,
pub name: String,
pub code: Option<String>,
pub distribution_strategy: Option<DistributionStrategy>,
pub url: Option<String>,
pub platforms: Option<Vec<Platform>>,
pub permissions: Option<Vec<String>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub created: String,
pub updated: String,
pub account_id: Option<String>,
}
impl Product {
pub(crate) fn from(data: KeygenResponseData<ProductAttributes>) -> Product {
Product {
id: data.id,
name: data.attributes.name,
code: data.attributes.code,
distribution_strategy: data.attributes.distribution_strategy,
url: data.attributes.url,
platforms: data.attributes.platforms,
permissions: data.attributes.permissions,
metadata: data.attributes.metadata,
created: data.attributes.created,
updated: data.attributes.updated,
account_id: data
.relationships
.account
.as_ref()
.and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
}
}
pub async fn create(request: CreateProductRequest) -> Result<Product, Error> {
let client = Client::from_global_config()?;
let mut attributes = serde_json::Map::new();
attributes.insert("name".to_string(), serde_json::json!(request.name));
if let Some(code) = &request.code {
if !code.is_empty() {
attributes.insert("code".to_string(), serde_json::json!(code));
}
}
insert_optional(
&mut attributes,
"distributionStrategy",
request.distribution_strategy,
)?;
insert_optional(&mut attributes, "url", request.url)?;
insert_optional(&mut attributes, "platforms", request.platforms)?;
insert_optional(&mut attributes, "permissions", request.permissions)?;
insert_optional(&mut attributes, "metadata", request.metadata)?;
let body = serde_json::json!({
"data": {
"type": "products",
"attributes": attributes
}
});
let response = client.post("products", Some(&body), None::<&()>).await?;
let product_response: ProductResponse = serde_json::from_value(response.body)?;
Ok(Product::from(product_response.data))
}
pub async fn list(options: Option<ListProductsOptions>) -> Result<Vec<Product>, Error> {
let client = Client::from_global_config()?;
let response = client.get("products", options.as_ref()).await?;
let products_response: ProductsResponse = serde_json::from_value(response.body)?;
Ok(products_response
.data
.into_iter()
.map(Product::from)
.collect())
}
pub async fn get(id: &str) -> Result<Product, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("products/{id}");
let response = client.get(&endpoint, None::<&()>).await?;
let product_response: ProductResponse = serde_json::from_value(response.body)?;
Ok(Product::from(product_response.data))
}
pub async fn update(&self, request: UpdateProductRequest) -> Result<Product, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("products/{}", self.id);
let mut attributes = serde_json::Map::new();
insert_optional(&mut attributes, "name", request.name)?;
insert_optional(&mut attributes, "code", request.code)?;
insert_optional(
&mut attributes,
"distributionStrategy",
request.distribution_strategy,
)?;
insert_optional(&mut attributes, "url", request.url)?;
insert_optional(&mut attributes, "platforms", request.platforms)?;
insert_optional(&mut attributes, "permissions", request.permissions)?;
insert_optional(&mut attributes, "metadata", request.metadata)?;
let body = serde_json::json!({
"data": {
"type": "products",
"attributes": attributes
}
});
let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
let product_response: ProductResponse = serde_json::from_value(response.body)?;
Ok(Product::from(product_response.data))
}
pub async fn delete(&self) -> Result<(), Error> {
let client = Client::from_global_config()?;
let endpoint = format!("products/{}", self.id);
client.delete::<(), ()>(&endpoint, None::<&()>).await?;
Ok(())
}
pub async fn generate_token(&self) -> Result<String, Error> {
let token = self.generate_token_with_options(None).await?;
token.token.ok_or_else(|| Error::KeygenApiError {
code: "INVALID_RESPONSE".to_string(),
detail: "Token response did not include a token value".to_string(),
body: serde_json::json!({ "id": token.id }),
})
}
pub async fn generate_token_with_options(
&self,
request: Option<CreateTokenRequest>,
) -> Result<Token, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("products/{}/tokens", self.id);
let attributes = token_request_attributes(request.as_ref())?;
let body = serde_json::json!({
"data": {
"type": "tokens",
"attributes": attributes
}
});
let response = client.post(&endpoint, Some(&body), None::<&()>).await?;
let token_response: TokenResponse = serde_json::from_value(response.body)?;
Ok(Token::from(token_response.data))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
};
#[test]
fn test_product_account_relationship() {
let product_data = KeygenResponseData {
id: "test-product-id".to_string(),
r#type: "products".to_string(),
attributes: ProductAttributes {
name: "Test Product".to_string(),
code: Some("test-product".to_string()),
distribution_strategy: Some(DistributionStrategy::Open),
url: Some("https://example.com".to_string()),
platforms: Some(vec![Platform::Windows, Platform::MacOs]),
permissions: Some(vec![
"license.read".to_string(),
"license.create".to_string(),
]),
metadata: Some(HashMap::new()),
created: "2023-01-01T00:00:00Z".to_string(),
updated: "2023-01-01T00:00:00Z".to_string(),
},
relationships: KeygenRelationships {
account: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "accounts".to_string(),
id: "test-account-id".to_string(),
}),
links: None,
}),
..Default::default()
},
};
let product = Product::from(product_data);
assert_eq!(product.account_id, Some("test-account-id".to_string()));
assert_eq!(product.id, "test-product-id");
assert_eq!(product.name, "Test Product");
}
#[test]
fn test_product_without_account_relationship() {
let product_data = KeygenResponseData {
id: "test-product-id".to_string(),
r#type: "products".to_string(),
attributes: ProductAttributes {
name: "Test Product".to_string(),
code: Some("test-product".to_string()),
distribution_strategy: Some(DistributionStrategy::Open),
url: None,
platforms: None,
permissions: None,
metadata: None,
created: "2023-01-01T00:00:00Z".to_string(),
updated: "2023-01-01T00:00:00Z".to_string(),
},
relationships: KeygenRelationships::default(),
};
let product = Product::from(product_data);
assert_eq!(product.account_id, None);
}
#[test]
fn test_platform_serialization() {
assert_eq!(
serde_json::to_string(&Platform::Windows).unwrap(),
"\"windows\""
);
assert_eq!(
serde_json::to_string(&Platform::MacOs).unwrap(),
"\"macOS\""
);
assert_eq!(
serde_json::to_string(&Platform::Linux).unwrap(),
"\"linux\""
);
assert_eq!(
serde_json::to_string(&Platform::Darwin).unwrap(),
"\"darwin\""
);
assert_eq!(
serde_json::to_string(&Platform::Android).unwrap(),
"\"android\""
);
assert_eq!(serde_json::to_string(&Platform::Ios).unwrap(), "\"iOS\"");
assert_eq!(serde_json::to_string(&Platform::Web).unwrap(), "\"web\"");
assert_eq!(
serde_json::to_string(&Platform::Other("embedded".to_string())).unwrap(),
"\"embedded\""
);
}
#[test]
fn test_platform_deserialization() {
assert_eq!(
serde_json::from_str::<Platform>("\"windows\"").unwrap(),
Platform::Windows
);
assert_eq!(
serde_json::from_str::<Platform>("\"macOS\"").unwrap(),
Platform::MacOs
);
assert_eq!(
serde_json::from_str::<Platform>("\"linux\"").unwrap(),
Platform::Linux
);
assert_eq!(
serde_json::from_str::<Platform>("\"darwin\"").unwrap(),
Platform::Darwin
);
assert_eq!(
serde_json::from_str::<Platform>("\"android\"").unwrap(),
Platform::Android
);
assert_eq!(
serde_json::from_str::<Platform>("\"iOS\"").unwrap(),
Platform::Ios
);
assert_eq!(
serde_json::from_str::<Platform>("\"web\"").unwrap(),
Platform::Web
);
}
#[test]
fn test_platform_custom_value_deserialization() {
let custom: Platform = serde_json::from_str("\"embedded\"").unwrap();
assert_eq!(custom, Platform::Other("embedded".to_string()));
let custom: Platform = serde_json::from_str("\"custom_device\"").unwrap();
assert_eq!(custom, Platform::Other("custom_device".to_string()));
}
#[test]
fn test_platform_custom_value_roundtrip() {
let original = Platform::Other("my-custom-platform".to_string());
let json = serde_json::to_string(&original).unwrap();
let deserialized: Platform = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_multiple_platforms_deserialization() {
let json = r#"["windows", "iOS", "embedded"]"#;
let platforms: Vec<Platform> = serde_json::from_str(json).unwrap();
assert_eq!(
platforms,
vec![
Platform::Windows,
Platform::Ios,
Platform::Other("embedded".to_string()),
]
);
}
}