Skip to main content

wowlab_cli/commands/snapshot/
mod.rs

1mod assisted_rotas;
2pub(crate) mod db;
3mod decode_loadout;
4mod dump_assisted;
5mod dump_spell;
6mod dump_trait;
7mod sync;
8
9use anyhow::Result;
10use clap::{
11    Subcommand,
12    builder::{PossibleValue, PossibleValuesParser, TypedValueParser},
13    error::ErrorKind,
14};
15use wowlab_fs::path::PathBuf;
16use wowlab_types::table_registry::GameDataTable;
17
18#[rustfmt::skip]
19pub(crate) use sync::{SyncCredentials, run_sync_with_credentials};
20
21/// Snapshot command selected by the top-level CLI.
22#[derive(Debug, Subcommand)]
23#[non_exhaustive]
24pub enum SnapshotCommand {
25    /// Sync transformed data to Supabase Postgres.
26    Sync(SyncArgs),
27    /// Dump a single spell to JSON (for debugging).
28    DumpSpell(DumpSpellArgs),
29    /// Dump a trait tree to JSON (for debugging).
30    DumpTrait(DumpTraitArgs),
31    /// Dump assisted rotation data to JSON.
32    DumpAssisted(DumpAssistedArgs),
33    /// Transform assisted rotations into engine format and dump/upsert them.
34    AssistedRotas(AssistedRotasArgs),
35    /// Decode a talent loadout string to JSON.
36    DecodeLoadout(DecodeLoadoutArgs),
37}
38
39impl SnapshotCommand {
40    /// Executes the selected snapshot command.
41    ///
42    /// # Errors
43    /// Returns an error if the selected snapshot command fails.
44    pub async fn run(self) -> Result<()> {
45        match self {
46            Self::Sync(args) => sync::run_sync(args).await,
47            Self::DumpSpell(args) => dump_spell::run_dump_spell(&args),
48            Self::DumpTrait(args) => dump_trait::run_dump_trait(&args),
49            Self::DumpAssisted(args) => dump_assisted::run_dump_assisted(&args),
50            Self::AssistedRotas(args) => assisted_rotas::run_assisted_rotas(args).await,
51            Self::DecodeLoadout(args) => decode_loadout::run_decode_loadout(&args),
52        }
53    }
54}
55
56#[derive(Clone)]
57struct GameDataTableValueParser {
58    possible_values: PossibleValuesParser,
59}
60
61impl GameDataTableValueParser {
62    fn new() -> Self {
63        let values = GameDataTable::iter().map(|table| PossibleValue::new(table.command_name()));
64
65        Self {
66            possible_values: PossibleValuesParser::new(values),
67        }
68    }
69}
70
71impl TypedValueParser for GameDataTableValueParser {
72    type Value = GameDataTable;
73
74    fn parse_ref(
75        &self,
76        cmd: &clap::Command,
77        arg: Option<&clap::Arg>,
78        value: &std::ffi::OsStr,
79    ) -> std::result::Result<Self::Value, clap::Error> {
80        let value = self.possible_values.parse_ref(cmd, arg, value)?;
81
82        value.parse::<GameDataTable>().map_err(|parse_error| {
83            clap::Error::raw(ErrorKind::InvalidValue, parse_error.to_string())
84        })
85    }
86
87    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
88        self.possible_values.possible_values()
89    }
90}
91
92/// Arguments for transforming and synchronizing game-data tables.
93#[derive(Debug, clap::Args)]
94pub struct SyncArgs {
95    /// Patch version to tag data with (e.g., "11.2.0")
96    #[arg(long)]
97    pub patch: String,
98
99    /// Directory containing DBC CSV files.
100    #[arg(long, default_value = "./data")]
101    pub data_dir: PathBuf,
102
103    /// Dry run - transform but don't write to database.
104    #[arg(long)]
105    pub dry_run: bool,
106
107    /// Only export + upload the NDJSON snapshot and publish game.meta; skip all table writes.
108    #[arg(long)]
109    pub json_only: bool,
110
111    /// Only sync specific tables (can be specified multiple times). If not specified, all tables are synced.
112    #[arg(long = "table", value_parser = GameDataTableValueParser::new())]
113    pub tables: Vec<GameDataTable>,
114}
115
116/// Arguments for dumping one transformed spell.
117#[derive(Debug, clap::Args)]
118pub struct DumpSpellArgs {
119    /// Spell ID to dump.
120    pub spell_id: i32,
121
122    /// Directory containing DBC CSV files.
123    #[arg(long, default_value = "./data")]
124    pub data_dir: PathBuf,
125}
126
127/// Arguments for dumping one specialization's trait tree.
128#[derive(Debug, clap::Args)]
129pub struct DumpTraitArgs {
130    /// Spec ID to dump.
131    pub spec_id: i32,
132
133    /// Directory containing DBC CSV files.
134    #[arg(long, default_value = "./data")]
135    pub data_dir: PathBuf,
136}
137
138/// Arguments for dumping raw assisted-rotation data.
139#[derive(Debug, clap::Args)]
140pub struct DumpAssistedArgs {
141    /// Optional spec ID filter (if omitted, dumps all specs).
142    #[arg(long)]
143    pub spec_id: Option<i32>,
144
145    /// Directory containing DBC CSV files.
146    #[arg(long, default_value = "./data")]
147    pub data_dir: PathBuf,
148
149    /// Optional output directory (writes JSON files instead of stdout).
150    #[arg(long)]
151    pub out_dir: Option<PathBuf>,
152}
153
154/// Destination policy for transformed assisted rotations.
155#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
156#[non_exhaustive]
157pub enum AssistedRotasMode {
158    /// Write rotation JSON files only.
159    Dump,
160    /// Upsert rotations to Supabase only.
161    Upsert,
162    /// Write files and upsert the same rotations.
163    Both,
164}
165
166/// Arguments for transforming assisted rotations.
167#[derive(Debug, clap::Args)]
168pub struct AssistedRotasArgs {
169    /// Directory containing DBC CSV files.
170    #[arg(long, default_value = "./data")]
171    pub data_dir: PathBuf,
172
173    /// Optional spec ID filter (if omitted, includes all assisted specs).
174    #[arg(long)]
175    pub spec_id: Option<i32>,
176
177    /// Output mode: dump files, upsert to Supabase, or both.
178    #[arg(long, value_enum, default_value_t = AssistedRotasMode::Dump)]
179    pub mode: AssistedRotasMode,
180
181    /// Output directory for engine rotation JSON dumps.
182    #[arg(long, default_value = "dumps/engine-assisted-rotas")]
183    pub out_dir: PathBuf,
184
185    /// Rotation owner user ID for upsert mode (UUID).
186    #[arg(long)]
187    pub user_id: Option<String>,
188
189    /// Prefix for generated rotation slugs.
190    #[arg(long, default_value = "assisted")]
191    pub slug_prefix: String,
192
193    /// Prefix for generated rotation names.
194    #[arg(long, default_value = "Assisted")]
195    pub name_prefix: String,
196
197    /// Mark upserted rotations as public.
198    #[arg(long)]
199    pub is_public: bool,
200}
201
202/// Arguments for decoding and optionally enriching a talent loadout.
203#[derive(Debug, clap::Args)]
204pub struct DecodeLoadoutArgs {
205    /// Base64-encoded talent loadout string.
206    pub loadout: String,
207
208    /// Directory containing DBC CSV files (enriches output with spell names).
209    #[arg(long)]
210    pub data_dir: Option<PathBuf>,
211}