feat: complete bark battle draft publish flow
This commit is contained in:
@@ -21,8 +21,9 @@ use shared_kernel::{
|
||||
offset_datetime_to_unix_micros, parse_rfc3339,
|
||||
};
|
||||
use spacetime_client::{
|
||||
BarkBattleDraftCreateRecordInput, BarkBattleRunFinishRecordInput, BarkBattleRunRecord,
|
||||
BarkBattleRunStartRecordInput, BarkBattleWorkPublishRecordInput, SpacetimeClientError,
|
||||
BarkBattleDraftConfigUpsertRecordInput, BarkBattleDraftCreateRecordInput,
|
||||
BarkBattleRunFinishRecordInput, BarkBattleRunRecord, BarkBattleRunStartRecordInput,
|
||||
BarkBattleWorkPublishRecordInput, SpacetimeClientError,
|
||||
};
|
||||
use time::{Duration as TimeDuration, OffsetDateTime};
|
||||
|
||||
@@ -73,11 +74,8 @@ struct BarkBattleRunSnapshotRecord {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BarkBattleDraftConfigSnapshotRecord {
|
||||
draft_id: String,
|
||||
#[allow(dead_code)]
|
||||
work_id: String,
|
||||
#[allow(dead_code)]
|
||||
config_version: u64,
|
||||
#[allow(dead_code)]
|
||||
ruleset_version: String,
|
||||
#[serde(default)]
|
||||
config_json: String,
|
||||
@@ -105,6 +103,35 @@ pub async fn create_bark_battle_draft(
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = bark_battle_json(payload, &request_context)?;
|
||||
let now = current_utc_micros();
|
||||
let editor_config = BarkBattleConfigEditorPayload {
|
||||
title: payload.title.clone(),
|
||||
description: payload.description.clone(),
|
||||
theme_preset: payload.theme_preset.clone(),
|
||||
player_dog_skin_preset: payload.player_dog_skin_preset.clone(),
|
||||
opponent_dog_skin_preset: payload.opponent_dog_skin_preset.clone(),
|
||||
player_character_image_src: normalize_optional_bark_battle_asset_source(
|
||||
&request_context,
|
||||
payload.player_character_image_src.as_deref(),
|
||||
"playerCharacterImageSrc",
|
||||
)?,
|
||||
opponent_character_image_src: normalize_optional_bark_battle_asset_source(
|
||||
&request_context,
|
||||
payload.opponent_character_image_src.as_deref(),
|
||||
"opponentCharacterImageSrc",
|
||||
)?,
|
||||
ui_background_image_src: normalize_optional_bark_battle_asset_source(
|
||||
&request_context,
|
||||
payload.ui_background_image_src.as_deref(),
|
||||
"uiBackgroundImageSrc",
|
||||
)?,
|
||||
bark_sound_src: normalize_optional_bark_battle_asset_source(
|
||||
&request_context,
|
||||
payload.bark_sound_src.as_deref(),
|
||||
"barkSoundSrc",
|
||||
)?,
|
||||
difficulty_preset: payload.difficulty_preset.clone(),
|
||||
leaderboard_enabled: payload.leaderboard_enabled,
|
||||
};
|
||||
let draft = state
|
||||
.spacetime_client()
|
||||
.create_bark_battle_draft(BarkBattleDraftCreateRecordInput {
|
||||
@@ -127,7 +154,35 @@ pub async fn create_bark_battle_draft(
|
||||
.map_err(|error| {
|
||||
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
|
||||
})?;
|
||||
let draft = map_draft_config_record(draft, &request_context)?;
|
||||
let draft_snapshot = parse_draft_snapshot_record(draft, &request_context)?;
|
||||
let config_json = serde_json::to_string(&editor_config).map_err(|error| {
|
||||
bark_battle_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
|
||||
"message": format!("Bark Battle config JSON 序列化失败: {error}"),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
let updated = state
|
||||
.spacetime_client()
|
||||
.update_bark_battle_draft_config(BarkBattleDraftConfigUpsertRecordInput {
|
||||
draft_id: draft_snapshot.draft_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
work_id: draft_snapshot.work_id,
|
||||
config_version: draft_snapshot.config_version.saturating_add(1),
|
||||
ruleset_version: draft_snapshot.ruleset_version,
|
||||
difficulty_preset: difficulty_to_spacetime_string(&editor_config.difficulty_preset)
|
||||
.to_string(),
|
||||
leaderboard_enabled: editor_config.leaderboard_enabled,
|
||||
config_json,
|
||||
updated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
|
||||
})?;
|
||||
let draft = map_draft_config_record(updated, &request_context)?;
|
||||
Ok(json_success_body(Some(&request_context), draft))
|
||||
}
|
||||
|
||||
@@ -139,13 +194,17 @@ pub async fn publish_bark_battle_work(
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = bark_battle_json(payload, &request_context)?;
|
||||
ensure_non_empty(&request_context, &payload.draft_id, "draftId")?;
|
||||
let work_id = payload
|
||||
let Some(work_id) = payload
|
||||
.work_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| build_prefixed_uuid_id(BARK_BATTLE_WORK_ID_PREFIX));
|
||||
.map(ToString::to_string) else {
|
||||
return Err(bark_battle_bad_request(
|
||||
&request_context,
|
||||
"workId 缺失,请重新生成草稿后再发布。",
|
||||
));
|
||||
};
|
||||
let published_snapshot_json = payload
|
||||
.published_snapshot
|
||||
.as_ref()
|
||||
@@ -473,11 +532,18 @@ fn map_draft_config_record(
|
||||
let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?;
|
||||
Ok(BarkBattleDraftConfig {
|
||||
draft_id: snapshot.draft_id,
|
||||
work_id: Some(snapshot.work_id),
|
||||
config_version: Some(snapshot.config_version.min(u64::from(u32::MAX)) as u32),
|
||||
ruleset_version: Some(snapshot.ruleset_version),
|
||||
title: editor_config.title,
|
||||
description: editor_config.description,
|
||||
theme_preset: editor_config.theme_preset,
|
||||
player_dog_skin_preset: editor_config.player_dog_skin_preset,
|
||||
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
|
||||
player_character_image_src: editor_config.player_character_image_src,
|
||||
opponent_character_image_src: editor_config.opponent_character_image_src,
|
||||
ui_background_image_src: editor_config.ui_background_image_src,
|
||||
bark_sound_src: editor_config.bark_sound_src,
|
||||
difficulty_preset: editor_config.difficulty_preset,
|
||||
leaderboard_enabled: editor_config.leaderboard_enabled,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
@@ -505,6 +571,10 @@ fn map_runtime_config_record(
|
||||
theme_preset: editor_config.theme_preset,
|
||||
player_dog_skin_preset: editor_config.player_dog_skin_preset,
|
||||
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
|
||||
player_character_image_src: editor_config.player_character_image_src,
|
||||
opponent_character_image_src: editor_config.opponent_character_image_src,
|
||||
ui_background_image_src: editor_config.ui_background_image_src,
|
||||
bark_sound_src: editor_config.bark_sound_src,
|
||||
leaderboard_enabled: editor_config.leaderboard_enabled,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
})
|
||||
@@ -527,6 +597,10 @@ fn map_published_config_record(
|
||||
theme_preset: editor_config.theme_preset,
|
||||
player_dog_skin_preset: editor_config.player_dog_skin_preset,
|
||||
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
|
||||
player_character_image_src: editor_config.player_character_image_src,
|
||||
opponent_character_image_src: editor_config.opponent_character_image_src,
|
||||
ui_background_image_src: editor_config.ui_background_image_src,
|
||||
bark_sound_src: editor_config.bark_sound_src,
|
||||
difficulty_preset: editor_config.difficulty_preset,
|
||||
leaderboard_enabled: editor_config.leaderboard_enabled,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
@@ -592,6 +666,23 @@ fn ensure_non_empty(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_optional_bark_battle_asset_source(
|
||||
request_context: &RequestContext,
|
||||
value: Option<&str>,
|
||||
field_name: &str,
|
||||
) -> Result<Option<String>, Response> {
|
||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value.chars().count() > 512 {
|
||||
return Err(bark_battle_bad_request(
|
||||
request_context,
|
||||
&format!("{field_name} 不能超过 512 个字符"),
|
||||
));
|
||||
}
|
||||
Ok(Some(value.to_string()))
|
||||
}
|
||||
|
||||
fn bark_battle_bad_request(request_context: &RequestContext, message: &str) -> Response {
|
||||
bark_battle_error_response(
|
||||
request_context,
|
||||
@@ -753,6 +844,7 @@ fn format_rfc3339_or_timestamp_micros(micros: i64) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn unit_and_energy_are_clamped_to_spacetime_millis() {
|
||||
@@ -773,4 +865,43 @@ mod tests {
|
||||
1_713_672_001_234_567
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_config_mapping_includes_stable_work_identity() {
|
||||
let request_context = RequestContext::new(
|
||||
"test-request".to_string(),
|
||||
"POST /api/creation/bark-battle/drafts".to_string(),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
);
|
||||
let config_json = json!({
|
||||
"title": "汪汪测试杯",
|
||||
"description": "",
|
||||
"themePreset": "sunny-yard",
|
||||
"playerDogSkinPreset": "主角",
|
||||
"opponentDogSkinPreset": "对手",
|
||||
"difficultyPreset": "normal",
|
||||
"leaderboardEnabled": true
|
||||
})
|
||||
.to_string();
|
||||
let row = json!({
|
||||
"draftId": "bark-battle-draft-1",
|
||||
"workId": "bark-battle-work-1",
|
||||
"configVersion": 2,
|
||||
"rulesetVersion": "bark-battle-ruleset-v1",
|
||||
"configJson": config_json,
|
||||
"updatedAtMicros": 1_713_686_401_234_567i64,
|
||||
});
|
||||
|
||||
let draft = map_draft_config_record(row, &request_context)
|
||||
.expect("draft config should map from SpacetimeDB snapshot");
|
||||
|
||||
assert_eq!(draft.draft_id, "bark-battle-draft-1");
|
||||
assert_eq!(draft.work_id.as_deref(), Some("bark-battle-work-1"));
|
||||
assert_eq!(draft.config_version, Some(2));
|
||||
assert_eq!(
|
||||
draft.ruleset_version.as_deref(),
|
||||
Some("bark-battle-ruleset-v1")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,19 @@ pub enum BarkBattleFinishStatus {
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleReplacementConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleConfigEditorPayload {
|
||||
@@ -39,6 +52,14 @@ pub struct BarkBattleConfigEditorPayload {
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
@@ -53,6 +74,14 @@ pub struct BarkBattleDraftCreateRequest {
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
@@ -66,6 +95,10 @@ impl From<BarkBattleDraftCreateRequest> for BarkBattleConfigEditorPayload {
|
||||
theme_preset: value.theme_preset,
|
||||
player_dog_skin_preset: value.player_dog_skin_preset,
|
||||
opponent_dog_skin_preset: value.opponent_dog_skin_preset,
|
||||
player_character_image_src: value.player_character_image_src,
|
||||
opponent_character_image_src: value.opponent_character_image_src,
|
||||
ui_background_image_src: value.ui_background_image_src,
|
||||
bark_sound_src: value.bark_sound_src,
|
||||
difficulty_preset: value.difficulty_preset,
|
||||
leaderboard_enabled: value.leaderboard_enabled,
|
||||
}
|
||||
@@ -86,12 +119,26 @@ pub struct BarkBattleWorkPublishRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleDraftConfig {
|
||||
pub draft_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub work_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_version: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ruleset_version: Option<String>,
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
@@ -102,11 +149,18 @@ impl Default for BarkBattleDraftConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
draft_id: String::new(),
|
||||
work_id: None,
|
||||
config_version: None,
|
||||
ruleset_version: None,
|
||||
title: String::new(),
|
||||
description: None,
|
||||
theme_preset: String::new(),
|
||||
player_dog_skin_preset: String::new(),
|
||||
opponent_dog_skin_preset: String::new(),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
leaderboard_enabled: true,
|
||||
updated_at: String::new(),
|
||||
@@ -129,6 +183,14 @@ pub struct BarkBattlePublishedConfig {
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub updated_at: String,
|
||||
@@ -151,6 +213,14 @@ pub struct BarkBattleRuntimeConfig {
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -409,7 +479,22 @@ mod tests {
|
||||
fn optional_fields_are_omitted_when_absent() {
|
||||
let draft = BarkBattleDraftConfig::default();
|
||||
let payload = serde_json::to_value(draft).expect("draft should serialize");
|
||||
assert!(!payload.as_object().unwrap().contains_key("workId"));
|
||||
assert!(!payload.as_object().unwrap().contains_key("configVersion"));
|
||||
assert!(!payload.as_object().unwrap().contains_key("rulesetVersion"));
|
||||
assert!(!payload.as_object().unwrap().contains_key("description"));
|
||||
assert!(
|
||||
!payload
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.contains_key("playerCharacterImageSrc")
|
||||
);
|
||||
assert!(
|
||||
!payload
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.contains_key("uiBackgroundImageSrc")
|
||||
);
|
||||
|
||||
let response = BarkBattlePersonalHistoryResponse {
|
||||
work_id: None,
|
||||
@@ -429,6 +514,74 @@ mod tests {
|
||||
assert!(!payload.as_object().unwrap().contains_key("bestSummary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_config_serializes_persistent_identity_fields() {
|
||||
let draft = BarkBattleDraftConfig {
|
||||
draft_id: "bark-battle-draft-1".to_string(),
|
||||
work_id: Some("bark-battle-work-1".to_string()),
|
||||
config_version: Some(2),
|
||||
ruleset_version: Some("bark-battle-ruleset-v1".to_string()),
|
||||
title: "汪汪测试杯".to_string(),
|
||||
description: None,
|
||||
theme_preset: "sunny-yard".to_string(),
|
||||
player_dog_skin_preset: "主角".to_string(),
|
||||
opponent_dog_skin_preset: "对手".to_string(),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
leaderboard_enabled: true,
|
||||
updated_at: "2026-05-14T10:00:00.000Z".to_string(),
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(draft).expect("draft should serialize");
|
||||
|
||||
assert_eq!(payload["draftId"], json!("bark-battle-draft-1"));
|
||||
assert_eq!(payload["workId"], json!("bark-battle-work-1"));
|
||||
assert_eq!(payload["configVersion"], json!(2));
|
||||
assert_eq!(
|
||||
payload["rulesetVersion"],
|
||||
json!("bark-battle-ruleset-v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacement_sources_serialize_as_camel_case_config_fields() {
|
||||
let config = BarkBattleConfigEditorPayload {
|
||||
title: "周末狗狗杯".to_string(),
|
||||
description: Some("轻配置草稿".to_string()),
|
||||
theme_preset: "neon-park".to_string(),
|
||||
player_dog_skin_preset: "shiba".to_string(),
|
||||
opponent_dog_skin_preset: "husky".to_string(),
|
||||
player_character_image_src: Some("/generated-bark-battle/player.png".to_string()),
|
||||
opponent_character_image_src: Some("https://example.test/opponent.png".to_string()),
|
||||
ui_background_image_src: Some("/generated-bark-battle/ui.png".to_string()),
|
||||
bark_sound_src: Some("/generated-bark-battle/bark.mp3".to_string()),
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Hard,
|
||||
leaderboard_enabled: true,
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(config).expect("config should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["playerCharacterImageSrc"],
|
||||
json!("/generated-bark-battle/player.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["opponentCharacterImageSrc"],
|
||||
json!("https://example.test/opponent.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["uiBackgroundImageSrc"],
|
||||
json!("/generated-bark-battle/ui.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["barkSoundSrc"],
|
||||
json!("/generated-bark-battle/bark.mp3")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_response_serializes_player_win_and_accepted() {
|
||||
let response = BarkBattleRunFinishResponse {
|
||||
|
||||
@@ -116,6 +116,10 @@ fn create_bark_battle_draft_tx(
|
||||
&input.opponent_dog_skin_preset,
|
||||
"opponent_dog_skin_preset",
|
||||
)?,
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: normalize_difficulty(input.difficulty_preset.as_deref())?,
|
||||
leaderboard_enabled: input.leaderboard_enabled.unwrap_or(true),
|
||||
};
|
||||
@@ -146,8 +150,8 @@ fn update_bark_battle_draft_config_tx(
|
||||
require_non_empty(&input.draft_id, "bark_battle draft_id")?;
|
||||
require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?;
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let editor_config = parse_editor_config(&input.config_json)?;
|
||||
validate_editor_config_snapshot(&editor_config)?;
|
||||
let mut editor_config = parse_editor_config(&input.config_json)?;
|
||||
normalize_editor_config_snapshot(&mut editor_config)?;
|
||||
if editor_config.difficulty_preset != input.difficulty_preset
|
||||
|| editor_config.leaderboard_enabled != input.leaderboard_enabled
|
||||
{
|
||||
@@ -171,7 +175,7 @@ fn update_bark_battle_draft_config_tx(
|
||||
row.ruleset_version = normalize_ruleset_version(&input.ruleset_version)?;
|
||||
row.difficulty_preset = normalize_difficulty(Some(&input.difficulty_preset))?;
|
||||
row.leaderboard_enabled = input.leaderboard_enabled;
|
||||
row.config_json = input.config_json;
|
||||
row.config_json = to_json_string(&editor_config);
|
||||
row.updated_at = updated_at;
|
||||
ctx.db
|
||||
.bark_battle_draft_config()
|
||||
@@ -523,12 +527,30 @@ fn require_non_empty(value: &str, label: &str) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_editor_config_snapshot(config: &BarkBattleEditorConfigSnapshot) -> Result<(), String> {
|
||||
normalize_title(Some(&config.title))?;
|
||||
normalize_required_preset(&config.theme_preset, "theme_preset")?;
|
||||
normalize_required_preset(&config.player_dog_skin_preset, "player_dog_skin_preset")?;
|
||||
normalize_required_preset(&config.opponent_dog_skin_preset, "opponent_dog_skin_preset")?;
|
||||
normalize_difficulty(Some(&config.difficulty_preset))?;
|
||||
fn normalize_editor_config_snapshot(
|
||||
config: &mut BarkBattleEditorConfigSnapshot,
|
||||
) -> Result<(), String> {
|
||||
config.title = normalize_title(Some(&config.title))?;
|
||||
config.theme_preset = normalize_required_preset(&config.theme_preset, "theme_preset")?;
|
||||
config.player_dog_skin_preset =
|
||||
normalize_required_preset(&config.player_dog_skin_preset, "player_dog_skin_preset")?;
|
||||
config.opponent_dog_skin_preset =
|
||||
normalize_required_preset(&config.opponent_dog_skin_preset, "opponent_dog_skin_preset")?;
|
||||
config.player_character_image_src = normalize_optional_asset_source(
|
||||
config.player_character_image_src.as_deref(),
|
||||
"player_character_image_src",
|
||||
)?;
|
||||
config.opponent_character_image_src = normalize_optional_asset_source(
|
||||
config.opponent_character_image_src.as_deref(),
|
||||
"opponent_character_image_src",
|
||||
)?;
|
||||
config.ui_background_image_src = normalize_optional_asset_source(
|
||||
config.ui_background_image_src.as_deref(),
|
||||
"ui_background_image_src",
|
||||
)?;
|
||||
config.bark_sound_src =
|
||||
normalize_optional_asset_source(config.bark_sound_src.as_deref(), "bark_sound_src")?;
|
||||
config.difficulty_preset = normalize_difficulty(Some(&config.difficulty_preset))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -555,6 +577,19 @@ fn normalize_required_preset(value: &str, field_name: &str) -> Result<String, St
|
||||
Ok(preset.to_string())
|
||||
}
|
||||
|
||||
fn normalize_optional_asset_source(
|
||||
value: Option<&str>,
|
||||
field_name: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value.chars().count() > 512 {
|
||||
return Err(format!("bark_battle {field_name} 不能超过 512 个字符"));
|
||||
}
|
||||
Ok(Some(value.to_string()))
|
||||
}
|
||||
|
||||
fn normalize_ruleset_version(value: &str) -> Result<String, String> {
|
||||
let ruleset = value.trim();
|
||||
if ruleset != BARK_BATTLE_DEFAULT_RULESET_VERSION {
|
||||
|
||||
@@ -117,6 +117,14 @@ pub struct BarkBattleEditorConfigSnapshot {
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user