Skip to main content

perl_lsp_feature_profile_cli/
lib.rs

1#![warn(missing_docs)]
2//! CLI profile-token parsing for Perl LSP feature governance.
3//!
4//! This microcrate owns one responsibility: parse user-facing profile tokens
5//! and expose diagnostics helpers for invalid values.
6
7use std::fmt;
8
9pub use perl_lsp_feature_policy::FeatureProfile;
10use perl_lsp_feature_profile::parse_profile_token;
11
12/// Parse a `--feature-profile` argument into a runtime policy.
13///
14/// Returns a structured error when the raw token is unknown. The error includes
15/// the current supported token set for CLI/user-facing diagnostics.
16pub fn parse_feature_profile_arg(
17    raw_profile: &str,
18) -> Result<FeatureProfile, UnsupportedFeatureProfileError> {
19    match parse_profile_token(raw_profile) {
20        Some(kind) => Ok(FeatureProfile::from_kind(kind)),
21        None => Err(UnsupportedFeatureProfileError { raw_profile: raw_profile.to_string() }),
22    }
23}
24
25/// Parse a profile argument and fall back to `FeatureProfile::current()`.
26pub fn parse_feature_profile_arg_or_current(raw_profile: &str) -> FeatureProfile {
27    parse_feature_profile_arg(raw_profile).unwrap_or_else(|_| FeatureProfile::current())
28}
29
30/// Canonical profile label for logs and diagnostics.
31pub const fn feature_profile_label(profile: FeatureProfile) -> &'static str {
32    profile.as_str()
33}
34
35/// Supported CLI tokens accepted by the profile parser.
36pub const fn feature_profile_supported_tokens() -> &'static [&'static str] {
37    FeatureProfile::supported_cli_profiles()
38}
39
40/// Structured parse error for invalid profile tokens.
41#[derive(Debug)]
42pub struct UnsupportedFeatureProfileError {
43    /// Raw token that could not be resolved.
44    pub raw_profile: String,
45}
46
47impl UnsupportedFeatureProfileError {
48    /// Human-friendly error message with support list.
49    pub fn message(&self) -> String {
50        let supported = feature_profile_supported_tokens().join(", ");
51        format!("Invalid feature profile: {}. Supported: {}", self.raw_profile, supported)
52    }
53}
54
55impl fmt::Display for UnsupportedFeatureProfileError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str(&self.message())
58    }
59}
60
61impl std::error::Error for UnsupportedFeatureProfileError {}
62
63#[cfg(test)]
64mod tests {
65    use super::{
66        FeatureProfile, UnsupportedFeatureProfileError, feature_profile_label,
67        feature_profile_supported_tokens, parse_feature_profile_arg,
68        parse_feature_profile_arg_or_current,
69    };
70    use perl_tdd_support::{must, must_err};
71
72    #[test]
73    fn parse_feature_profile_accepts_known_aliases() {
74        let profile = must(parse_feature_profile_arg("ga_lock"));
75        assert_eq!(profile.as_str(), "ga-lock");
76
77        let profile = must(parse_feature_profile_arg("Prod"));
78        assert_eq!(profile.as_str(), "production");
79
80        let profile = must(parse_feature_profile_arg("  ALL  "));
81        assert_eq!(profile.as_str(), "all");
82    }
83
84    #[test]
85    fn parse_unknown_profile_falls_back_to_current() {
86        let profile = parse_feature_profile_arg_or_current("unknown-profile");
87        assert_eq!(profile, FeatureProfile::current());
88    }
89
90    #[test]
91    fn supported_tokens_contain_expected() {
92        let supported = feature_profile_supported_tokens();
93        assert!(supported.contains(&"auto"));
94        assert!(supported.contains(&"ga"));
95        assert!(supported.contains(&"prod"));
96        assert!(supported.contains(&"all"));
97    }
98
99    // ── Error diagnostics ───────────────────────────────────────────
100
101    #[test]
102    fn parse_feature_profile_arg_returns_error_for_unknown() {
103        let err = must_err(parse_feature_profile_arg("bogus"));
104        assert!(err.raw_profile == "bogus", "error should capture the raw profile token");
105    }
106
107    #[test]
108    fn unsupported_error_message_contains_raw_token() {
109        let err = UnsupportedFeatureProfileError { raw_profile: "xyzzy".to_string() };
110        let msg = err.message();
111        assert!(msg.contains("xyzzy"), "error message should contain the raw token");
112    }
113
114    #[test]
115    fn unsupported_error_message_lists_supported_tokens() {
116        let err = UnsupportedFeatureProfileError { raw_profile: "bad".to_string() };
117        let msg = err.message();
118        assert!(msg.contains("auto"), "error message should list 'auto'");
119        assert!(msg.contains("prod"), "error message should list 'prod'");
120        assert!(msg.contains("all"), "error message should list 'all'");
121    }
122
123    #[test]
124    fn unsupported_error_display_matches_message() {
125        let err = UnsupportedFeatureProfileError { raw_profile: "nope".to_string() };
126        let display = format!("{err}");
127        assert_eq!(display, err.message());
128    }
129
130    #[test]
131    fn unsupported_error_is_std_error() {
132        let err: Box<dyn std::error::Error> =
133            Box::new(UnsupportedFeatureProfileError { raw_profile: "test".to_string() });
134        let msg = format!("{err}");
135        assert!(msg.contains("test"));
136    }
137
138    // ── feature_profile_label ───────────────────────────────────────
139
140    #[test]
141    fn feature_profile_label_returns_canonical_name() {
142        assert_eq!(feature_profile_label(FeatureProfile::GaLock), "ga-lock");
143        assert_eq!(feature_profile_label(FeatureProfile::Production), "production");
144        assert_eq!(feature_profile_label(FeatureProfile::All), "all");
145    }
146
147    // ── parse_feature_profile_arg_or_current with valid input ───────
148
149    #[test]
150    fn parse_feature_profile_arg_or_current_accepts_valid() {
151        let profile = parse_feature_profile_arg_or_current("all");
152        assert_eq!(profile, FeatureProfile::All);
153    }
154
155    #[test]
156    fn parse_feature_profile_arg_or_current_with_whitespace() {
157        let profile = parse_feature_profile_arg_or_current("  ga  ");
158        assert_eq!(profile, FeatureProfile::GaLock);
159    }
160}