wowlab_cli/commands/snapshot/assisted_rotas/
output.rs1use anyhow::{Context, Result};
2use serde::Serialize;
3use uuid::Uuid;
4use wowlab_fs::{atomic, directory, path::Path};
5use wowlab_parsers::apply_rotation_overlay;
6use wowlab_types::game::SpecId;
7
8use super::{super::db, preparation::PreparedRotation};
9
10#[derive(Debug, Serialize)]
11struct DumpIndexEntry {
12 spec_id: i32,
13 name: String,
14 slug: String,
15 file: String,
16}
17
18pub(super) async fn upsert_rotations(
19 user_id: &str,
20 is_public: bool,
21 rotations: &[PreparedRotation],
22) -> Result<()> {
23 let user_id = Uuid::parse_str(user_id).context("invalid --user-id UUID")?;
24 let database_url = std::env::var("DATABASE_URL").context("DATABASE_URL not set")?;
25 let pool = db::connect(&database_url).await?;
26 let mut transaction = pool.begin().await?;
27
28 for rotation in rotations {
29 let script = match u32::try_from(rotation.spec_id)
30 .ok()
31 .and_then(SpecId::from_wow_spec_id)
32 {
33 Some(spec) => apply_rotation_overlay(spec.slug(), &rotation.script)?,
34 None => rotation.script.clone(),
35 };
36
37 sqlx::query_file!(
38 "queries/snapshot_assisted_rotas_upsert.sql",
39 user_id,
40 &rotation.slug,
41 &rotation.name,
42 &script,
43 &rotation.description,
44 is_public,
45 rotation.spec_id
46 )
47 .execute(&mut *transaction)
48 .await?;
49 }
50
51 transaction.commit().await?;
52 println!(
53 "Upserted {} assisted rotations to public.rotations",
54 rotations.len()
55 );
56
57 Ok(())
58}
59
60pub(super) fn write_rotation_dumps(out_dir: &Path, rotations: &[PreparedRotation]) -> Result<()> {
61 directory::ensure(out_dir)?;
62 let mut index = Vec::with_capacity(rotations.len());
63
64 for rotation in rotations {
65 let file = match u32::try_from(rotation.spec_id)
66 .ok()
67 .and_then(SpecId::from_wow_spec_id)
68 {
69 Some(spec) => format!("{}_assisted.json", spec.slug()),
70 None => format!("spec_{}_assisted.json", rotation.spec_id),
71 };
72 let path = out_dir.join(&file);
73
74 let contents = serde_json::to_string_pretty(&rotation.script)?;
75
76 atomic::replace(&path, contents)?;
77 println!("{}", path.display());
78 index.push(DumpIndexEntry {
79 spec_id: rotation.spec_id,
80 name: rotation.name.clone(),
81 slug: rotation.slug.clone(),
82 file,
83 });
84 }
85
86 let index_path = out_dir.join("index.json");
87 let contents = serde_json::to_string_pretty(&index)?;
88
89 atomic::replace(&index_path, contents)?;
90 println!(
91 "Wrote {} engine rotation dumps to {}",
92 rotations.len(),
93 out_dir.display()
94 );
95
96 Ok(())
97}
98
99#[cfg(test)]
100mod tests {
101 use googletest::prelude::*;
102 use serde_json::json;
103 use wowlab_fs::{directory, file, temporary::Directory};
104
105 use super::{PreparedRotation, write_rotation_dumps};
106
107 #[gtest]
108 fn rotation_dump_preserves_json_bytes_and_writes_index_atomically() -> Result<()> {
109 let output = Directory::new().or_fail()?;
110 let rotations = [PreparedRotation {
111 spec_id: 999_999,
112 name: "Test".to_string(),
113 slug: "test".to_string(),
114 description: "test rotation".to_string(),
115 script: json!({ "spell": "fireball" }),
116 }];
117
118 write_rotation_dumps(output.path(), &rotations).or_fail()?;
119
120 verify_that!(
121 file::read_text(&output.path().join("spec_999999_assisted.json")).or_fail()?,
122 eq("{\n \"spell\": \"fireball\"\n}")
123 )?;
124 verify_that!(
125 file::read_text(&output.path().join("index.json")).or_fail()?,
126 eq(
127 "[\n {\n \"spec_id\": 999999,\n \"name\": \"Test\",\n \
128 \"slug\": \"test\",\n \"file\": \"spec_999999_assisted.json\"\n }\n]"
129 )
130 )?;
131
132 verify_that!(directory::entries(output.path()).or_fail()?, len(eq(2)))
133 }
134}