Skip to main content

ic_query/cloud_engine/
text.rs

1//! Module: cloud_engine::text
2//!
3//! Responsibility: render compact human-facing CloudEngine reports.
4//! Does not own: report construction, JSON output, live calls, or process output.
5//! Boundary: formats cycle amounts only for text while JSON retains raw decimal fields.
6
7#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
8use super::{CloudEngineListReport, CloudEngineOperatorLookupStatus};
9use super::{CloudEngineOperatorReport, CloudEnginePricesReport, CloudEngineReportContext};
10use crate::{
11    human_quantity::decimal_cycle_count_text,
12    table::{ColumnAlign, render_table},
13    text_value::{optional_text, sanitize_text, yes_no},
14};
15
16/// Render the Registry CloudEngine inventory and separate operator-binding observations.
17#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
18#[must_use]
19pub fn cloud_engine_list_report_text(report: &CloudEngineListReport) -> String {
20    let mut lines = list_context_lines(report);
21    lines.push(String::new());
22    lines.push("CloudEngine subnets".to_string());
23    if report.cloud_engines.is_empty() {
24        lines.push("none".to_string());
25        return lines.join("\n");
26    }
27    lines.push(list_table(report));
28    append_lookup_failures(report, &mut lines);
29    lines.join("\n")
30}
31
32#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
33fn list_context_lines(report: &CloudEngineListReport) -> Vec<String> {
34    vec![
35        format!("network: {}", sanitize_text(&report.network)),
36        format!(
37            "registry_authority: {}",
38            sanitize_text(&report.registry_authority)
39        ),
40        format!("registry_canister_id: {}", report.registry_canister_id),
41        format!("registry_version: {}", report.registry_version),
42        format!("registry_assurance: {}", report.registry_assurance.as_str()),
43        format!(
44            "registry_source_endpoints: {}",
45            report
46                .registry_source_endpoints
47                .iter()
48                .map(|endpoint| sanitize_text(endpoint))
49                .collect::<Vec<_>>()
50                .join(", ")
51        ),
52        format!(
53            "catalog_fetched_at: {}",
54            sanitize_text(&report.catalog_fetched_at)
55        ),
56        format!(
57            "catalog_cache_disposition: {}",
58            report.catalog_cache_disposition.as_str()
59        ),
60        format!("catalog_stale: {}", yes_no(report.catalog_stale)),
61        String::new(),
62        format!(
63            "control_plane_authority: {}",
64            sanitize_text(&report.control_plane_authority)
65        ),
66        format!(
67            "control_plane_canister_id: {}",
68            report.control_plane_canister_id
69        ),
70        format!(
71            "control_plane_source_endpoint: {}",
72            sanitize_text(&report.control_plane_source_endpoint)
73        ),
74        format!(
75            "control_plane_fetched_at: {}",
76            sanitize_text(&report.control_plane_fetched_at)
77        ),
78        format!(
79            "control_plane_certified: {}",
80            yes_no(report.control_plane_certified)
81        ),
82        format!(
83            "control_plane_point_in_time_guaranteed: {}",
84            yes_no(report.control_plane_point_in_time_guaranteed)
85        ),
86        format!(
87            "control_plane_lookup_attempt_count: {}",
88            report.control_plane_lookup_attempt_count
89        ),
90        format!(
91            "operator_bindings: {} resolved, {} absent, {} failed",
92            report.operator_binding_count,
93            report.missing_operator_binding_count,
94            report.operator_lookup_failure_count
95        ),
96    ]
97}
98
99#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
100fn list_table(report: &CloudEngineListReport) -> String {
101    let headers = ["Label", "Subnet", "Nodes", "Binding", "Operator"];
102    let alignments = [
103        ColumnAlign::Left,
104        ColumnAlign::Left,
105        ColumnAlign::Right,
106        ColumnAlign::Left,
107        ColumnAlign::Left,
108    ];
109    let rows = report
110        .cloud_engines
111        .iter()
112        .map(|row| {
113            [
114                sanitize_text(&row.subnet_label),
115                row.subnet_id.clone(),
116                row.node_count
117                    .map_or_else(|| "unknown".to_string(), |count| count.to_string()),
118                row.operator_lookup_status.as_str().to_string(),
119                row.operator_canister_id
120                    .clone()
121                    .unwrap_or_else(|| "-".to_string()),
122            ]
123        })
124        .collect::<Vec<_>>();
125    render_table(&headers, &rows, &alignments)
126}
127
128#[cfg(all(feature = "cloud-engine-host", feature = "subnet-catalog-host"))]
129fn append_lookup_failures(report: &CloudEngineListReport, lines: &mut Vec<String>) {
130    let failures = report
131        .cloud_engines
132        .iter()
133        .filter(|row| row.operator_lookup_status == CloudEngineOperatorLookupStatus::Failed)
134        .collect::<Vec<_>>();
135    if !failures.is_empty() {
136        lines.push(String::new());
137        lines.push("Operator lookup failures".to_string());
138        lines.extend(failures.into_iter().map(|row| {
139            format!(
140                "  {}: {}",
141                row.subnet_id,
142                sanitize_text(
143                    row.operator_lookup_error
144                        .as_deref()
145                        .unwrap_or("unspecified lookup failure")
146                )
147            )
148        }));
149    }
150}
151
152/// Render one CloudEngine Subnet-to-operator report.
153#[must_use]
154pub fn cloud_engine_operator_report_text(report: &CloudEngineOperatorReport) -> String {
155    let mut lines = context_lines(&report.context);
156    lines.extend([
157        format!("subnet_id: {}", report.subnet_id),
158        format!(
159            "operator_binding_present: {}",
160            yes_no(report.operator_binding_present)
161        ),
162        format!(
163            "operator_canister_id: {}",
164            optional_text(report.operator_canister_id.as_ref())
165        ),
166        format!(
167            "engine_owner: {}",
168            optional_text(report.engine_owner.as_ref())
169        ),
170        format!(
171            "platform_admin: {}",
172            optional_text(report.platform_admin.as_ref())
173        ),
174        format!(
175            "caffeine_enabled: {}",
176            report
177                .caffeine_enabled
178                .map_or_else(|| "-".to_string(), |enabled| yes_no(enabled).to_string())
179        ),
180        format!(
181            "claimed_domain_count: {}",
182            report
183                .claimed_domain_count
184                .map_or_else(|| "-".to_string(), |count| count.to_string())
185        ),
186    ]);
187
188    if let Some(domains) = report.claimed_domains.as_ref()
189        && !domains.is_empty()
190    {
191        lines.push(String::new());
192        lines.push("Claimed domains".to_string());
193        lines.extend(
194            domains
195                .iter()
196                .map(|domain| format!("  {}", sanitize_text(domain))),
197        );
198    }
199    lines.join("\n")
200}
201
202/// Render one bounded CloudEngine marketplace price report.
203#[must_use]
204pub fn cloud_engine_prices_report_text(report: &CloudEnginePricesReport) -> String {
205    let mut lines = context_lines(&report.context);
206    lines.extend([
207        format!("network_fee: {}", report.network_fee),
208        format!("price_count: {}", report.price_count),
209    ]);
210
211    if !report.prices.is_empty() {
212        let headers = [
213            "Key",
214            "Node type",
215            "Data center",
216            "Provider",
217            "Net/month",
218            "Gross/month",
219            "Updated (ns)",
220        ];
221        let alignments = [
222            ColumnAlign::Left,
223            ColumnAlign::Left,
224            ColumnAlign::Left,
225            ColumnAlign::Left,
226            ColumnAlign::Right,
227            ColumnAlign::Right,
228            ColumnAlign::Right,
229        ];
230        let rows = report
231            .prices
232            .iter()
233            .map(|row| {
234                [
235                    row.key.clone(),
236                    row.node_type.to_string(),
237                    row.data_center_id
238                        .as_deref()
239                        .map_or_else(|| "-".to_string(), sanitize_text),
240                    row.provider_id.clone().unwrap_or_else(|| "-".to_string()),
241                    decimal_cycle_count_text(&row.net_cycles_per_month),
242                    decimal_cycle_count_text(&row.gross_cycles_per_month),
243                    row.updated_at_unix_nanos.to_string(),
244                ]
245            })
246            .collect::<Vec<_>>();
247        lines.push(String::new());
248        lines.push("Marketplace prices".to_string());
249        lines.push(render_table(&headers, &rows, &alignments));
250    }
251    lines.join("\n")
252}
253
254fn context_lines(context: &CloudEngineReportContext) -> Vec<String> {
255    vec![
256        format!("network: {}", sanitize_text(&context.network)),
257        format!("authority: {}", sanitize_text(&context.authority)),
258        format!("engine_canister_id: {}", context.engine_canister_id),
259        format!("fetched_at: {}", sanitize_text(&context.fetched_at)),
260        format!(
261            "source_endpoint: {}",
262            sanitize_text(&context.source_endpoint)
263        ),
264        format!("fetched_by: {}", sanitize_text(&context.fetched_by)),
265        format!("certified: {}", yes_no(context.certified)),
266        format!(
267            "point_in_time_guaranteed: {}",
268            yes_no(context.point_in_time_guaranteed)
269        ),
270        format!("query_call_count: {}", context.query_call_count),
271    ]
272}