Skip to content

Commit 2bf3ebd

Browse files
authored
Merge pull request #128 from stephenleo/story-3-4-remove-cfg-test-type-aliases-for-old-config-types
refactor(config): remove #[cfg(test)] type aliases for old config types
2 parents d573141 + d9f9946 commit 2bf3ebd

File tree

4 files changed

+302
-603
lines changed

4 files changed

+302
-603
lines changed

src/config.rs

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,18 @@ pub struct CostConfig {
4848
pub critical_style: Option<String>,
4949
pub format: Option<String>,
5050
// Sub-field per-display configs (map to [cship.cost.total_cost_usd] etc.)
51-
pub total_cost_usd: Option<CostSubfieldConfig>,
52-
pub total_duration_ms: Option<CostSubfieldConfig>,
53-
pub total_api_duration_ms: Option<CostSubfieldConfig>,
54-
pub total_lines_added: Option<CostSubfieldConfig>,
55-
pub total_lines_removed: Option<CostSubfieldConfig>,
51+
pub total_cost_usd: Option<SubfieldConfig>,
52+
pub total_duration_ms: Option<SubfieldConfig>,
53+
pub total_api_duration_ms: Option<SubfieldConfig>,
54+
pub total_lines_added: Option<SubfieldConfig>,
55+
pub total_lines_removed: Option<SubfieldConfig>,
5656
}
5757

58-
/// Configuration for individual `[cship.cost.*]` sub-field modules.
58+
/// Unified configuration for individual sub-field modules
59+
/// (e.g. `[cship.cost.total_cost_usd]`, `[cship.context_window.used_percentage]`).
5960
#[derive(Debug, Deserialize, Default)]
60-
pub struct CostSubfieldConfig {
61+
pub struct SubfieldConfig {
6162
pub style: Option<String>,
62-
/// Reserved — not yet rendered; included for config schema consistency.
63-
pub symbol: Option<String>,
64-
pub disabled: Option<bool>,
65-
/// Reserved — not yet rendered; included for config schema consistency.
66-
pub label: Option<String>,
67-
pub warn_threshold: Option<f64>,
68-
pub warn_style: Option<String>,
69-
pub critical_threshold: Option<f64>,
70-
pub critical_style: Option<String>,
71-
pub format: Option<String>,
72-
}
73-
74-
/// Configuration for individual `[cship.context_window.*]` sub-field modules.
75-
#[derive(Debug, Deserialize, Default)]
76-
pub struct ContextWindowSubfieldConfig {
77-
pub style: Option<String>,
78-
/// Used only in the format path (via `$symbol`); ignored in the default render path.
7963
pub symbol: Option<String>,
8064
pub disabled: Option<bool>,
8165
pub warn_threshold: Option<f64>,
@@ -97,6 +81,26 @@ pub struct ContextWindowSubfieldConfig {
9781
pub invert_threshold: Option<bool>,
9882
}
9983

84+
/// Trait for uniform access to style/threshold fields shared by config types.
85+
/// Used by `render_styled_value()` to resolve sub-field → parent fallback.
86+
///
87+
/// `format_str()` and `symbol_str()` default to `None`. Only parent configs
88+
/// whose sub-fields should inherit format/symbol (i.e., `ContextWindowConfig`)
89+
/// override these.
90+
pub trait HasThresholdStyle {
91+
fn style(&self) -> Option<&str>;
92+
fn warn_threshold(&self) -> Option<f64>;
93+
fn warn_style(&self) -> Option<&str>;
94+
fn critical_threshold(&self) -> Option<f64>;
95+
fn critical_style(&self) -> Option<&str>;
96+
fn format_str(&self) -> Option<&str> {
97+
None
98+
}
99+
fn symbol_str(&self) -> Option<&str> {
100+
None
101+
}
102+
}
103+
100104
/// Configuration for `[cship.context_bar]` — visual progress bar with thresholds.
101105
/// Implemented in Story 2.2. Defined here so all Epic 2 config is available.
102106
#[derive(Debug, Deserialize, Default)]
@@ -127,16 +131,40 @@ pub struct ContextWindowConfig {
127131
pub critical_style: Option<String>,
128132
pub format: Option<String>,
129133
// Per-sub-field configs (map to [cship.context_window.used_percentage] etc.)
130-
pub used_percentage: Option<ContextWindowSubfieldConfig>,
131-
pub remaining_percentage: Option<ContextWindowSubfieldConfig>,
132-
pub size: Option<ContextWindowSubfieldConfig>,
133-
pub total_input_tokens: Option<ContextWindowSubfieldConfig>,
134-
pub total_output_tokens: Option<ContextWindowSubfieldConfig>,
135-
pub current_usage_input_tokens: Option<ContextWindowSubfieldConfig>,
136-
pub current_usage_output_tokens: Option<ContextWindowSubfieldConfig>,
137-
pub current_usage_cache_creation_input_tokens: Option<ContextWindowSubfieldConfig>,
138-
pub current_usage_cache_read_input_tokens: Option<ContextWindowSubfieldConfig>,
139-
pub used_tokens: Option<ContextWindowSubfieldConfig>,
134+
pub used_percentage: Option<SubfieldConfig>,
135+
pub remaining_percentage: Option<SubfieldConfig>,
136+
pub size: Option<SubfieldConfig>,
137+
pub total_input_tokens: Option<SubfieldConfig>,
138+
pub total_output_tokens: Option<SubfieldConfig>,
139+
pub current_usage_input_tokens: Option<SubfieldConfig>,
140+
pub current_usage_output_tokens: Option<SubfieldConfig>,
141+
pub current_usage_cache_creation_input_tokens: Option<SubfieldConfig>,
142+
pub current_usage_cache_read_input_tokens: Option<SubfieldConfig>,
143+
pub used_tokens: Option<SubfieldConfig>,
144+
}
145+
146+
impl HasThresholdStyle for ContextWindowConfig {
147+
fn style(&self) -> Option<&str> {
148+
self.style.as_deref()
149+
}
150+
fn warn_threshold(&self) -> Option<f64> {
151+
self.warn_threshold
152+
}
153+
fn warn_style(&self) -> Option<&str> {
154+
self.warn_style.as_deref()
155+
}
156+
fn critical_threshold(&self) -> Option<f64> {
157+
self.critical_threshold
158+
}
159+
fn critical_style(&self) -> Option<&str> {
160+
self.critical_style.as_deref()
161+
}
162+
fn format_str(&self) -> Option<&str> {
163+
self.format.as_deref()
164+
}
165+
fn symbol_str(&self) -> Option<&str> {
166+
self.symbol.as_deref()
167+
}
140168
}
141169

142170
/// Configuration for `[cship.vim]` — vim mode display.

src/format.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,85 @@
1010
//!
1111
//! Source: architecture.md#Core Architectural Decisions, epics.md#Story 2.5
1212
13+
/// Centralized style/threshold/format rendering for sub-field render functions.
14+
///
15+
/// Resolves style, thresholds, and format strings using sub-field → parent fallback,
16+
/// handling `invert_threshold` for decreasing-health indicators (e.g. remaining_percentage).
17+
///
18+
/// # Arguments
19+
/// - `val_str`: Already-formatted display string (e.g. `"85"`, `"0.0123"`)
20+
/// - `threshold_val`: Numeric value for threshold comparison; `None` for non-threshold fields
21+
/// - `sub_cfg`: The sub-field's own `SubfieldConfig` (may be `None`)
22+
/// - `parent`: Parent config implementing `HasThresholdStyle` for fallback (may be `None`)
23+
///
24+
/// # Returns
25+
/// `None` when the format path renders empty (conditional group with absent `$value`).
26+
/// `Some(styled_string)` otherwise.
27+
pub fn render_styled_value(
28+
val_str: &str,
29+
threshold_val: Option<f64>,
30+
sub_cfg: Option<&crate::config::SubfieldConfig>,
31+
parent: Option<&dyn crate::config::HasThresholdStyle>,
32+
) -> Option<String> {
33+
// Resolve all fields with sub → parent fallback
34+
let style = sub_cfg
35+
.and_then(|c| c.style.as_deref())
36+
.or_else(|| parent.and_then(|p| p.style()));
37+
let mut effective_val = threshold_val;
38+
let mut warn_threshold = sub_cfg
39+
.and_then(|c| c.warn_threshold)
40+
.or_else(|| parent.and_then(|p| p.warn_threshold()));
41+
let mut warn_style = sub_cfg
42+
.and_then(|c| c.warn_style.as_deref())
43+
.or_else(|| parent.and_then(|p| p.warn_style()));
44+
let mut critical_threshold = sub_cfg
45+
.and_then(|c| c.critical_threshold)
46+
.or_else(|| parent.and_then(|p| p.critical_threshold()));
47+
let mut critical_style = sub_cfg
48+
.and_then(|c| c.critical_style.as_deref())
49+
.or_else(|| parent.and_then(|p| p.critical_style()));
50+
51+
// Inverted thresholds: use sub-only values (negated) and negate the comparison value.
52+
// Parent thresholds are in the non-inverted domain and must not be inherited.
53+
if sub_cfg.and_then(|c| c.invert_threshold).unwrap_or(false) {
54+
effective_val = threshold_val.map(|v| -v);
55+
warn_threshold = sub_cfg.and_then(|c| c.warn_threshold).map(|t| -t);
56+
warn_style = sub_cfg.and_then(|c| c.warn_style.as_deref());
57+
critical_threshold = sub_cfg.and_then(|c| c.critical_threshold).map(|t| -t);
58+
critical_style = sub_cfg.and_then(|c| c.critical_style.as_deref());
59+
}
60+
61+
// Format path: resolve symbol, threshold style, then apply format
62+
let fmt = sub_cfg
63+
.and_then(|c| c.format.as_deref())
64+
.or_else(|| parent.and_then(|p| p.format_str()));
65+
if let Some(fmt) = fmt {
66+
let symbol = sub_cfg
67+
.and_then(|c| c.symbol.as_deref())
68+
.or_else(|| parent.and_then(|p| p.symbol_str()));
69+
let effective_style = crate::ansi::resolve_threshold_style(
70+
effective_val,
71+
style,
72+
warn_threshold,
73+
warn_style,
74+
critical_threshold,
75+
critical_style,
76+
);
77+
return apply_module_format(fmt, Some(val_str), symbol, effective_style);
78+
}
79+
80+
// Default path: apply_style_with_threshold handles no-threshold gracefully
81+
Some(crate::ansi::apply_style_with_threshold(
82+
val_str,
83+
effective_val,
84+
style,
85+
warn_threshold,
86+
warn_style,
87+
critical_threshold,
88+
critical_style,
89+
))
90+
}
91+
1392
/// Apply a per-module format string.
1493
///
1594
/// # Arguments
@@ -150,6 +229,91 @@ fn find_matching_close(s: &str, start: usize, open: char, close: char) -> Option
150229
mod tests {
151230
use super::*;
152231

232+
// --- render_styled_value tests ---
233+
234+
#[test]
235+
fn test_render_styled_value_with_format_string() {
236+
let sub = crate::config::SubfieldConfig {
237+
format: Some("[$value]($style)".to_string()),
238+
style: Some("bold green".to_string()),
239+
..Default::default()
240+
};
241+
let result = render_styled_value("85", Some(85.0), Some(&sub), None);
242+
let s = result.unwrap();
243+
assert!(s.contains("85"), "value present: {s:?}");
244+
assert!(s.contains('\x1b'), "ANSI codes present: {s:?}");
245+
}
246+
247+
#[test]
248+
fn test_render_styled_value_no_format_threshold_above_warn() {
249+
let sub = crate::config::SubfieldConfig {
250+
warn_threshold: Some(50.0),
251+
warn_style: Some("yellow".to_string()),
252+
critical_threshold: Some(90.0),
253+
critical_style: Some("bold red".to_string()),
254+
..Default::default()
255+
};
256+
let result = render_styled_value("75", Some(75.0), Some(&sub), None);
257+
let s = result.unwrap();
258+
assert!(s.contains("75"), "value present: {s:?}");
259+
assert!(s.contains('\x1b'), "ANSI codes for warn style: {s:?}");
260+
}
261+
262+
#[test]
263+
fn test_render_styled_value_no_format_no_threshold() {
264+
let sub = crate::config::SubfieldConfig {
265+
style: Some("cyan".to_string()),
266+
..Default::default()
267+
};
268+
let result = render_styled_value("hello", None, Some(&sub), None);
269+
let s = result.unwrap();
270+
assert!(s.contains("hello"), "value present: {s:?}");
271+
assert!(s.contains('\x1b'), "ANSI codes for base style: {s:?}");
272+
}
273+
274+
#[test]
275+
fn test_render_styled_value_invert_threshold() {
276+
// invert_threshold: value 20 with warn_threshold 30 → inverted: -20 >= -30 → warn fires
277+
let sub = crate::config::SubfieldConfig {
278+
invert_threshold: Some(true),
279+
warn_threshold: Some(30.0),
280+
warn_style: Some("yellow".to_string()),
281+
critical_threshold: Some(10.0),
282+
critical_style: Some("bold red".to_string()),
283+
..Default::default()
284+
};
285+
let result = render_styled_value("20", Some(20.0), Some(&sub), None);
286+
let s = result.unwrap();
287+
assert!(s.contains("20"), "value present: {s:?}");
288+
assert!(s.contains('\x1b'), "ANSI codes for inverted warn: {s:?}");
289+
}
290+
291+
#[test]
292+
fn test_render_styled_value_parent_fallback_style() {
293+
let sub = crate::config::SubfieldConfig {
294+
..Default::default()
295+
};
296+
let parent = crate::config::ContextWindowConfig {
297+
style: Some("bold magenta".to_string()),
298+
..Default::default()
299+
};
300+
let result = render_styled_value(
301+
"42",
302+
None,
303+
Some(&sub),
304+
Some(&parent as &dyn crate::config::HasThresholdStyle),
305+
);
306+
let s = result.unwrap();
307+
assert!(s.contains("42"), "value present: {s:?}");
308+
assert!(s.contains('\x1b'), "ANSI codes from parent style: {s:?}");
309+
}
310+
311+
#[test]
312+
fn test_render_styled_value_no_sub_no_parent() {
313+
let result = render_styled_value("plain", None, None, None);
314+
assert_eq!(result, Some("plain".to_string()));
315+
}
316+
153317
#[test]
154318
fn test_dollar_value_substituted() {
155319
let result = apply_module_format("$value", Some("35"), None, None);

0 commit comments

Comments
 (0)