winbrew_app\operations\update/
planner.rs

1use anyhow::Result;
2
3use crate::models::catalog::CatalogMetadata;
4
5use super::metadata::metadata_url_for_snapshot_url;
6use super::types::{CatalogDownloadPlan, CatalogUpdateMode, CatalogUpdateResponse};
7
8/// Converts the update API response into a concrete catalog download plan.
9///
10/// The planner is intentionally small and deterministic: it validates the API
11/// payload against the caller's local metadata, then maps the response into one
12/// of three outcomes.
13///
14/// - `Current`: the catalog is already up to date and no download is needed.
15/// - `Full`: the workflow should download a full snapshot plus its metadata.
16/// - `Patch`: the workflow should apply one or more incremental SQL patches to
17///   the existing local catalog.
18///
19/// The function returns `Ok(None)` when the response is incomplete or
20/// incompatible with the local catalog state, which lets the caller fall back
21/// to a full snapshot request.
22pub(super) fn plan_catalog_download(
23    local_metadata: Option<&CatalogMetadata>,
24    selection: CatalogUpdateResponse,
25) -> Result<Option<CatalogDownloadPlan>> {
26    match selection.mode {
27        CatalogUpdateMode::Current => {
28            let current_hash = if selection.current.trim().is_empty() {
29                selection.target.clone()
30            } else {
31                selection.current.clone()
32            };
33
34            if current_hash.trim().is_empty() {
35                return Ok(None);
36            }
37
38            let target_hash = if selection.target.trim().is_empty() {
39                current_hash.clone()
40            } else {
41                selection.target
42            };
43
44            Ok(Some(CatalogDownloadPlan::Current {
45                current_hash,
46                target_hash,
47            }))
48        }
49        CatalogUpdateMode::Full => {
50            if selection.target.trim().is_empty() {
51                return Ok(None);
52            }
53
54            let catalog_url = match selection.snapshot {
55                Some(snapshot) if !snapshot.trim().is_empty() => snapshot,
56                _ => return Ok(None),
57            };
58
59            let metadata_url = metadata_url_for_snapshot_url(&catalog_url)?;
60
61            Ok(Some(CatalogDownloadPlan::Full {
62                catalog_url,
63                metadata_url,
64                expected_hash: Some(selection.target),
65            }))
66        }
67        CatalogUpdateMode::Patch => {
68            if selection.target.trim().is_empty() {
69                return Ok(None);
70            }
71
72            let local_metadata = match local_metadata {
73                Some(metadata) => metadata,
74                None => return Ok(None),
75            };
76
77            if local_metadata.current_hash != selection.current {
78                return Ok(None);
79            }
80
81            if selection.patches.is_empty()
82                || selection.patches.iter().any(|url| url.trim().is_empty())
83            {
84                return Ok(None);
85            }
86
87            Ok(Some(CatalogDownloadPlan::Patch {
88                patch_urls: selection.patches,
89                expected_hash: selection.target,
90            }))
91        }
92    }
93}