algocline_app/service/dist.rs
1//! `AppService::hub_dist` — facade that chains `hub_reindex` and
2//! `hub_gendoc` in a single call.
3//!
4//! This is the thin composition layer expected by downstream hub
5//! repositories that want to regenerate `hub_index.json` and the
6//! public documentation artifacts in one shot. It performs no
7//! filesystem work of its own — it calls the two underlying
8//! services in sequence and assembles their JSON responses into a
9//! `{ "reindex": ..., "gendoc": ... }` envelope.
10//!
11//! Error propagation (per `CLAUDE.md §Service 層 Error 伝播規律`):
12//!
13//! - If `hub_reindex` fails, this function returns immediately with
14//! `Err("dist: reindex failed: {inner}")` and `hub_gendoc` is not
15//! invoked. No `warn!`, no silent drop.
16//! - If `hub_gendoc` fails after a successful reindex, the returned
17//! `Err` text embeds the (already-succeeded) reindex JSON so the
18//! caller can observe both outcomes in a single response:
19//! `Err("dist: gendoc failed: {inner}\nreindex result (succeeded): {json}")`.
20//! The reindex-side side effect (the written `hub_index.json`) is
21//! not rolled back — callers must treat it as authoritative after
22//! a failed gendoc.
23//! - Any JSON parse failure on the underlying responses is also
24//! propagated with a `dist:` prefix.
25
26use super::hub_dist_preset::{preset_meta_value, resolve_hub_dist_preset};
27use super::AppService;
28
29impl AppService {
30 /// Run `hub_reindex` followed by `hub_gendoc` as a single call.
31 ///
32 /// See the module-level doc comment for error semantics. `source_dir`
33 /// is forwarded to both steps; `output_path` is the reindex
34 /// `hub_index.json` destination (callers typically point this at
35 /// `{source_dir}/hub_index.json`); the remaining arguments are
36 /// forwarded to `hub_gendoc` unchanged.
37 #[allow(clippy::too_many_arguments)]
38 pub fn hub_dist(
39 &self,
40 source_dir: &str,
41 output_path: Option<&str>,
42 out_dir: Option<&str>,
43 preset: Option<&str>,
44 project_root: Option<&str>,
45 projections: Option<&[String]>,
46 config_path: Option<&str>,
47 lint_strict: Option<bool>,
48 ) -> Result<String, String> {
49 let preset_resolution = resolve_hub_dist_preset(
50 preset,
51 project_root,
52 source_dir,
53 projections,
54 config_path,
55 lint_strict,
56 )
57 .map_err(|e| format!("dist: preset resolve failed: {e}"))?;
58
59 let eff_projections = preset_resolution.projections.as_deref();
60 let eff_config_path = preset_resolution.config_path.as_deref();
61 let eff_lint_strict = preset_resolution.lint_strict;
62
63 // Step 1: reindex. Propagate failure immediately — gendoc is
64 // not invoked when reindex cannot produce a fresh index.
65 let reindex_json = self
66 .hub_reindex(output_path, Some(source_dir))
67 .map_err(|e| format!("dist: reindex failed: {e}"))?;
68
69 // Step 2: gendoc. On failure, surface the reindex JSON so the
70 // caller sees both the succeeded-half and the failed-half.
71 let gendoc_json = match self.hub_gendoc(
72 source_dir,
73 out_dir,
74 eff_projections,
75 eff_config_path,
76 eff_lint_strict,
77 ) {
78 Ok(json) => json,
79 Err(e) => {
80 return Err(format!(
81 "dist: gendoc failed: {e}\nreindex result (succeeded): {reindex_json}"
82 ));
83 }
84 };
85
86 // Step 3: compose `{ reindex, gendoc }`.
87 let reindex_val: serde_json::Value = serde_json::from_str(&reindex_json)
88 .map_err(|e| format!("dist: reindex response parse: {e}"))?;
89 let gendoc_val: serde_json::Value = serde_json::from_str(&gendoc_json)
90 .map_err(|e| format!("dist: gendoc response parse: {e}"))?;
91
92 let mut composed = serde_json::json!({
93 "reindex": reindex_val,
94 "gendoc": gendoc_val,
95 "preset_catalog_version": preset_resolution.catalog_version,
96 });
97 if preset_resolution.preset_name.is_some() {
98 composed["preset"] = preset_meta_value(&preset_resolution);
99 }
100 Ok(composed.to_string())
101 }
102}