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 pub fn hub_dist(
38 &self,
39 source_dir: &str,
40 output_path: Option<&str>,
41 out_dir: Option<&str>,
42 preset: Option<&str>,
43 project_root: Option<&str>,
44 projections: Option<&[String]>,
45 config_path: Option<&str>,
46 lint_strict: Option<bool>,
47 ) -> Result<String, String> {
48 let preset_resolution = resolve_hub_dist_preset(
49 preset,
50 project_root,
51 source_dir,
52 projections,
53 config_path,
54 lint_strict,
55 )
56 .map_err(|e| format!("dist: preset resolve failed: {e}"))?;
57
58 let eff_projections = preset_resolution.projections.as_deref();
59 let eff_config_path = preset_resolution.config_path.as_deref();
60 let eff_lint_strict = preset_resolution.lint_strict;
61
62 // Step 1: reindex. Propagate failure immediately — gendoc is
63 // not invoked when reindex cannot produce a fresh index.
64 let reindex_json = self
65 .hub_reindex(output_path, Some(source_dir))
66 .map_err(|e| format!("dist: reindex failed: {e}"))?;
67
68 // Step 2: gendoc. On failure, surface the reindex JSON so the
69 // caller sees both the succeeded-half and the failed-half.
70 let gendoc_json = match self.hub_gendoc(
71 source_dir,
72 out_dir,
73 eff_projections,
74 eff_config_path,
75 eff_lint_strict,
76 ) {
77 Ok(json) => json,
78 Err(e) => {
79 return Err(format!(
80 "dist: gendoc failed: {e}\nreindex result (succeeded): {reindex_json}"
81 ));
82 }
83 };
84
85 // Step 3: compose `{ reindex, gendoc }`.
86 let reindex_val: serde_json::Value = serde_json::from_str(&reindex_json)
87 .map_err(|e| format!("dist: reindex response parse: {e}"))?;
88 let gendoc_val: serde_json::Value = serde_json::from_str(&gendoc_json)
89 .map_err(|e| format!("dist: gendoc response parse: {e}"))?;
90
91 let mut composed = serde_json::json!({
92 "reindex": reindex_val,
93 "gendoc": gendoc_val,
94 "preset_catalog_version": preset_resolution.catalog_version,
95 });
96 if preset_resolution.preset_name.is_some() {
97 composed["preset"] = preset_meta_value(&preset_resolution);
98 }
99 Ok(composed.to_string())
100 }
101}