-
Notifications
You must be signed in to change notification settings - Fork 1
WVDSH-1924: Add achievement list command #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
80bb9fb
fe539eb
8d2f61d
3b710d4
33eafe5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,41 @@ | ||||||||||||||||||||
| use crate::auth::require_api_key; | ||||||||||||||||||||
| use crate::config; | ||||||||||||||||||||
| use anyhow::{Context, Result}; | ||||||||||||||||||||
| use serde::Deserialize; | ||||||||||||||||||||
| use comfy_table::modifiers::UTF8_ROUND_CORNERS; | ||||||||||||||||||||
| use comfy_table::presets::UTF8_FULL; | ||||||||||||||||||||
| use comfy_table::{Cell, ContentArrangement, Table}; | ||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||
| use serde_json::json; | ||||||||||||||||||||
| use std::path::Path; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// The create response, narrowed to the fields printed by the command. This is | ||||||||||||||||||||
| /// deliberately separate from `Achievement`, whose list payload is larger. | ||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||
| struct CreatedAchievement { | ||||||||||||||||||||
| _id: String, | ||||||||||||||||||||
| identifier: String, | ||||||||||||||||||||
| #[serde(rename = "displayName")] | ||||||||||||||||||||
| display_name: String, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Deserialize, Serialize)] | ||||||||||||||||||||
| struct Achievement { | ||||||||||||||||||||
| _id: String, | ||||||||||||||||||||
| identifier: String, | ||||||||||||||||||||
| #[serde(rename = "displayName")] | ||||||||||||||||||||
| display_name: String, | ||||||||||||||||||||
| description: String, | ||||||||||||||||||||
| image: String, | ||||||||||||||||||||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||||||||||||||||||||
| secret: bool, | ||||||||||||||||||||
|
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Your E2E run confirms today's backend returns Cheap hardening:
Suggested change
Your
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The backend contract requires |
||||||||||||||||||||
| #[serde(rename = "statId", skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| stat_id: Option<String>, | ||||||||||||||||||||
| #[serde(rename = "statThreshold", skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| stat_threshold: Option<f64>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||
| struct AchievementsResponse { | ||||||||||||||||||||
| achievements: Vec<Achievement>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||
|
|
@@ -91,6 +116,65 @@ pub struct CreateAchievementArgs<'a> { | |||||||||||||||||||
| pub image_path: Option<&'a Path>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub async fn handle_achievement_list(game_id: &str, json: bool) -> Result<()> { | ||||||||||||||||||||
| let api_key = require_api_key()?; | ||||||||||||||||||||
| let client = config::create_http_client()?; | ||||||||||||||||||||
| let api_host = config::get("api_host")?; | ||||||||||||||||||||
| let url = format!("{}/api/games/{}/achievements", api_host, game_id); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let resp = client | ||||||||||||||||||||
| .get(&url) | ||||||||||||||||||||
| .header("Authorization", format!("Bearer {}", api_key)) | ||||||||||||||||||||
| .send() | ||||||||||||||||||||
| .await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let resp = config::check_api_response(resp).await?; | ||||||||||||||||||||
| let data: AchievementsResponse = resp.json().await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if json { | ||||||||||||||||||||
| println!("{}", serde_json::to_string_pretty(&data.achievements)?); | ||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if data.achievements.is_empty() { | ||||||||||||||||||||
| println!("No achievements found."); | ||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let mut table = Table::new(); | ||||||||||||||||||||
| table | ||||||||||||||||||||
| .load_preset(UTF8_FULL) | ||||||||||||||||||||
| .apply_modifier(UTF8_ROUND_CORNERS) | ||||||||||||||||||||
| .set_content_arrangement(ContentArrangement::Dynamic) | ||||||||||||||||||||
| .set_header(vec![ | ||||||||||||||||||||
| Cell::new("ID"), | ||||||||||||||||||||
| Cell::new("Identifier"), | ||||||||||||||||||||
| Cell::new("Title"), | ||||||||||||||||||||
| Cell::new("Description"), | ||||||||||||||||||||
| Cell::new("Secret"), | ||||||||||||||||||||
| Cell::new("Stat ID"), | ||||||||||||||||||||
| Cell::new("Threshold"), | ||||||||||||||||||||
| ]); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for achievement in data.achievements { | ||||||||||||||||||||
| table.add_row(vec![ | ||||||||||||||||||||
| achievement._id, | ||||||||||||||||||||
| achievement.identifier, | ||||||||||||||||||||
| achievement.display_name, | ||||||||||||||||||||
| achievement.description, | ||||||||||||||||||||
| (if achievement.secret { "yes" } else { "no" }).to_string(), | ||||||||||||||||||||
| achievement.stat_id.unwrap_or_else(|| "-".to_string()), | ||||||||||||||||||||
| achievement | ||||||||||||||||||||
| .stat_threshold | ||||||||||||||||||||
| .map(|threshold| threshold.to_string()) | ||||||||||||||||||||
| .unwrap_or_else(|| "-".to_string()), | ||||||||||||||||||||
| ]); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| println!("{table}"); | ||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub async fn handle_achievement_create(args: CreateAchievementArgs<'_>) -> Result<()> { | ||||||||||||||||||||
| let api_key = require_api_key()?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -133,7 +217,7 @@ pub async fn handle_achievement_create(args: CreateAchievementArgs<'_>) -> Resul | |||||||||||||||||||
| .await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let resp = config::check_api_response(resp).await?; | ||||||||||||||||||||
| let achievement: Achievement = resp.json().await?; | ||||||||||||||||||||
| let achievement: CreatedAchievement = resp.json().await?; | ||||||||||||||||||||
| println!( | ||||||||||||||||||||
| "✓ Created achievement \"{}\" (id: {}, identifier: {})", | ||||||||||||||||||||
| achievement.display_name, achievement._id, achievement.identifier | ||||||||||||||||||||
|
|
@@ -247,3 +331,73 @@ pub async fn handle_achievement_delete( | |||||||||||||||||||
| println!("✓ Deleted achievement {}", achievement_id); | ||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||
| mod tests { | ||||||||||||||||||||
| use super::*; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn parses_the_achievement_list_response() { | ||||||||||||||||||||
| let response: AchievementsResponse = serde_json::from_value(json!({ | ||||||||||||||||||||
| "achievements": [{ | ||||||||||||||||||||
| "_id": "achievement-id", | ||||||||||||||||||||
| "identifier": "FIRST_WIN", | ||||||||||||||||||||
| "displayName": "First Win", | ||||||||||||||||||||
| "description": "Win a match", | ||||||||||||||||||||
| "image": "achievements/first-win.png", | ||||||||||||||||||||
| "secret": false, | ||||||||||||||||||||
| "statId": "wins-stat-id", | ||||||||||||||||||||
| "statThreshold": 1 | ||||||||||||||||||||
| }] | ||||||||||||||||||||
| })) | ||||||||||||||||||||
| .expect("the API response should deserialize"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let achievement = &response.achievements[0]; | ||||||||||||||||||||
| assert_eq!(achievement._id, "achievement-id"); | ||||||||||||||||||||
| assert_eq!(achievement.identifier, "FIRST_WIN"); | ||||||||||||||||||||
| assert_eq!(achievement.display_name, "First Win"); | ||||||||||||||||||||
| assert_eq!(achievement.stat_id.as_deref(), Some("wins-stat-id")); | ||||||||||||||||||||
| assert_eq!(achievement.stat_threshold, Some(1.0)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
cloud9c marked this conversation as resolved.
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn parses_an_achievement_without_a_stat_link() { | ||||||||||||||||||||
| let response: AchievementsResponse = serde_json::from_value(json!({ | ||||||||||||||||||||
| "achievements": [{ | ||||||||||||||||||||
| "_id": "achievement-id", | ||||||||||||||||||||
| "identifier": "WELCOME", | ||||||||||||||||||||
| "displayName": "Welcome", | ||||||||||||||||||||
| "description": "Start the game", | ||||||||||||||||||||
| "image": "", | ||||||||||||||||||||
| "secret": true | ||||||||||||||||||||
| }] | ||||||||||||||||||||
| })) | ||||||||||||||||||||
| .expect("an achievement with no stat link should deserialize"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let achievement = &response.achievements[0]; | ||||||||||||||||||||
| assert!(achievement.secret); | ||||||||||||||||||||
| assert_eq!(achievement.stat_id, None); | ||||||||||||||||||||
| assert_eq!(achievement.stat_threshold, None); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[test] | ||||||||||||||||||||
| fn json_output_uses_api_field_names_and_omits_empty_stat_fields() { | ||||||||||||||||||||
| let achievement = Achievement { | ||||||||||||||||||||
| _id: "achievement-id".to_string(), | ||||||||||||||||||||
| identifier: "WELCOME".to_string(), | ||||||||||||||||||||
| display_name: "Welcome".to_string(), | ||||||||||||||||||||
| description: "Start the game".to_string(), | ||||||||||||||||||||
| image: "achievements/welcome.png".to_string(), | ||||||||||||||||||||
| secret: false, | ||||||||||||||||||||
| stat_id: None, | ||||||||||||||||||||
| stat_threshold: None, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let value = serde_json::to_value(achievement).expect("achievement should serialize"); | ||||||||||||||||||||
| assert_eq!(value["displayName"], "Welcome"); | ||||||||||||||||||||
| assert_eq!(value["image"], "achievements/welcome.png"); | ||||||||||||||||||||
| assert!(value.get("display_name").is_none()); | ||||||||||||||||||||
| assert!(value.get("statId").is_none()); | ||||||||||||||||||||
| assert!(value.get("statThreshold").is_none()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.