use crate::client::Client;
use crate::errors::Error;
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 ArtifactStatus {
Waiting,
Uploaded,
Failed,
Yanked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactAttributes {
pub filename: String,
pub filetype: Option<String>,
pub filesize: Option<u64>,
pub platform: Option<String>,
pub arch: Option<String>,
pub signature: Option<String>,
pub checksum: Option<String>,
pub status: ArtifactStatus,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub created: String,
pub updated: String,
#[serde(rename = "yanked")]
pub yanked_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ArtifactResponse {
pub data: KeygenResponseData<ArtifactAttributes>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ArtifactsResponse {
pub data: Vec<KeygenResponseData<ArtifactAttributes>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateArtifactRequest {
pub filename: String,
pub release_id: String,
pub filetype: Option<String>,
pub filesize: Option<u64>,
pub platform: Option<String>,
pub arch: Option<String>,
pub signature: Option<String>,
pub checksum: Option<String>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateArtifactRequest {
pub filename: Option<String>,
pub filetype: Option<String>,
pub platform: Option<String>,
pub arch: Option<String>,
pub signature: Option<String>,
pub checksum: Option<String>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListArtifactsOptions {
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<ArtifactStatus>,
}
#[derive(Debug, Clone)]
pub struct Artifact {
pub id: String,
pub filename: String,
pub filetype: Option<String>,
pub filesize: Option<u64>,
pub platform: Option<String>,
pub arch: Option<String>,
pub signature: Option<String>,
pub checksum: Option<String>,
pub status: ArtifactStatus,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub created: String,
pub updated: String,
pub yanked_at: Option<String>,
pub release_id: Option<String>,
pub product_id: Option<String>,
pub account_id: Option<String>,
}
impl Artifact {
pub(crate) fn from(data: KeygenResponseData<ArtifactAttributes>) -> Artifact {
Artifact {
id: data.id,
filename: data.attributes.filename,
filetype: data.attributes.filetype,
filesize: data.attributes.filesize,
platform: data.attributes.platform,
arch: data.attributes.arch,
signature: data.attributes.signature,
checksum: data.attributes.checksum,
status: data.attributes.status,
metadata: data.attributes.metadata,
created: data.attributes.created,
updated: data.attributes.updated,
yanked_at: data.attributes.yanked_at,
release_id: data
.relationships
.release
.as_ref()
.and_then(|r| r.data.as_ref().map(|d| d.id.clone())),
product_id: data
.relationships
.product
.as_ref()
.and_then(|p| p.data.as_ref().map(|d| d.id.clone())),
account_id: data
.relationships
.account
.as_ref()
.and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
}
}
pub async fn create(request: CreateArtifactRequest) -> Result<Artifact, Error> {
let client = Client::from_global_config()?;
let mut attributes = serde_json::Map::new();
attributes.insert(
"filename".to_string(),
serde_json::Value::String(request.filename),
);
if let Some(filetype) = request.filetype {
attributes.insert("filetype".to_string(), serde_json::Value::String(filetype));
}
if let Some(filesize) = request.filesize {
attributes.insert("filesize".to_string(), serde_json::json!(filesize));
}
if let Some(platform) = request.platform {
attributes.insert("platform".to_string(), serde_json::Value::String(platform));
}
if let Some(arch) = request.arch {
attributes.insert("arch".to_string(), serde_json::Value::String(arch));
}
if let Some(signature) = request.signature {
attributes.insert(
"signature".to_string(),
serde_json::Value::String(signature),
);
}
if let Some(checksum) = request.checksum {
attributes.insert("checksum".to_string(), serde_json::Value::String(checksum));
}
if let Some(metadata) = request.metadata {
attributes.insert("metadata".to_string(), serde_json::to_value(metadata)?);
}
let body = serde_json::json!({
"data": {
"type": "artifacts",
"attributes": attributes,
"relationships": {
"release": {
"data": {
"type": "releases",
"id": request.release_id
}
}
}
}
});
let response = client.post("artifacts", Some(&body), None::<&()>).await?;
let artifact_response: ArtifactResponse = serde_json::from_value(response.body)?;
Ok(Artifact::from(artifact_response.data))
}
pub async fn list(options: Option<ListArtifactsOptions>) -> Result<Vec<Artifact>, Error> {
let client = Client::from_global_config()?;
let response = client.get("artifacts", options.as_ref()).await?;
let artifacts_response: ArtifactsResponse = serde_json::from_value(response.body)?;
Ok(artifacts_response
.data
.into_iter()
.map(Artifact::from)
.collect())
}
pub async fn get(id: &str) -> Result<Artifact, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("artifacts/{id}");
let response = client.get(&endpoint, None::<&()>).await?;
let artifact_response: ArtifactResponse = serde_json::from_value(response.body)?;
Ok(Artifact::from(artifact_response.data))
}
pub async fn update(&self, request: UpdateArtifactRequest) -> Result<Artifact, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("artifacts/{}", self.id);
let mut attributes = serde_json::Map::new();
if let Some(filename) = request.filename {
attributes.insert("filename".to_string(), serde_json::Value::String(filename));
}
if let Some(filetype) = request.filetype {
attributes.insert("filetype".to_string(), serde_json::Value::String(filetype));
}
if let Some(platform) = request.platform {
attributes.insert("platform".to_string(), serde_json::Value::String(platform));
}
if let Some(arch) = request.arch {
attributes.insert("arch".to_string(), serde_json::Value::String(arch));
}
if let Some(signature) = request.signature {
attributes.insert(
"signature".to_string(),
serde_json::Value::String(signature),
);
}
if let Some(checksum) = request.checksum {
attributes.insert("checksum".to_string(), serde_json::Value::String(checksum));
}
if let Some(metadata) = request.metadata {
attributes.insert("metadata".to_string(), serde_json::to_value(metadata)?);
}
let body = serde_json::json!({
"data": {
"type": "artifacts",
"attributes": attributes
}
});
let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
let artifact_response: ArtifactResponse = serde_json::from_value(response.body)?;
Ok(Artifact::from(artifact_response.data))
}
pub async fn delete(&self) -> Result<(), Error> {
let client = Client::from_global_config()?;
let endpoint = format!("artifacts/{}", self.id);
client.delete::<(), ()>(&endpoint, None::<&()>).await?;
Ok(())
}
pub async fn yank(&self) -> Result<Artifact, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("artifacts/{}/actions/yank", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let artifact_response: ArtifactResponse = serde_json::from_value(response.body)?;
Ok(Artifact::from(artifact_response.data))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
};
#[test]
fn test_artifact_from_response_data() {
let artifact_data = KeygenResponseData {
id: "test-artifact-id".to_string(),
r#type: "artifacts".to_string(),
attributes: ArtifactAttributes {
filename: "app-1.0.0.dmg".to_string(),
filetype: Some("dmg".to_string()),
filesize: Some(10485760),
platform: Some("darwin".to_string()),
arch: Some("amd64".to_string()),
signature: None,
checksum: Some("abc123".to_string()),
status: ArtifactStatus::Uploaded,
metadata: Some(HashMap::new()),
created: "2023-01-01T00:00:00Z".to_string(),
updated: "2023-01-01T00:00:00Z".to_string(),
yanked_at: None,
},
relationships: KeygenRelationships {
release: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "releases".to_string(),
id: "test-release-id".to_string(),
}),
links: None,
}),
account: Some(KeygenRelationship {
data: Some(KeygenRelationshipData {
r#type: "accounts".to_string(),
id: "test-account-id".to_string(),
}),
links: None,
}),
..Default::default()
},
};
let artifact = Artifact::from(artifact_data);
assert_eq!(artifact.id, "test-artifact-id");
assert_eq!(artifact.filename, "app-1.0.0.dmg");
assert_eq!(artifact.filetype, Some("dmg".to_string()));
assert_eq!(artifact.filesize, Some(10485760));
assert_eq!(artifact.platform, Some("darwin".to_string()));
assert_eq!(artifact.arch, Some("amd64".to_string()));
assert_eq!(artifact.status, ArtifactStatus::Uploaded);
assert_eq!(artifact.release_id, Some("test-release-id".to_string()));
assert_eq!(artifact.account_id, Some("test-account-id".to_string()));
}
#[test]
fn test_artifact_status_serialization() {
assert_eq!(
serde_json::to_string(&ArtifactStatus::Waiting).unwrap(),
"\"WAITING\""
);
assert_eq!(
serde_json::to_string(&ArtifactStatus::Uploaded).unwrap(),
"\"UPLOADED\""
);
assert_eq!(
serde_json::to_string(&ArtifactStatus::Failed).unwrap(),
"\"FAILED\""
);
assert_eq!(
serde_json::to_string(&ArtifactStatus::Yanked).unwrap(),
"\"YANKED\""
);
}
}