perl_lsp_feature_contracts/
lib.rs1#![warn(missing_docs)]
2pub use perl_lsp_capability_map::{caps_from_feature_ids, feature_ids_from_caps};
10use serde::Serialize;
11
12#[derive(Debug, Clone, Copy, Serialize)]
14pub struct FeatureProfileSpec {
15 pub canonical: &'static str,
17 pub aliases: &'static [&'static str],
19 pub description: &'static str,
21}
22
23const GA_LOCK_ALIASES: &[&str] = &["ga-lock", "ga", "ga_lock"];
24const PRODUCTION_ALIASES: &[&str] = &["production", "prod"];
25const ALL_ALIASES: &[&str] = &["all"];
26
27#[derive(Debug, Clone, Copy, Eq, PartialEq)]
29pub enum FeatureProfileKind {
30 GaLock,
32 Production,
34 All,
36}
37
38impl FeatureProfileKind {
39 pub fn from_str_name(s: &str) -> Option<Self> {
41 match s {
42 "auto" => Some(Self::current()),
43 "ga-lock" | "ga" | "ga_lock" => Some(Self::GaLock),
44 "production" | "prod" => Some(Self::Production),
45 "all" => Some(Self::All),
46 _ => None,
47 }
48 }
49
50 pub const fn current() -> Self {
52 Self::from_ga_lock_enabled(cfg!(feature = "lsp-ga-lock"))
53 }
54
55 pub const fn from_ga_lock_enabled(ga_lock_enabled: bool) -> Self {
57 if ga_lock_enabled { Self::GaLock } else { Self::Production }
58 }
59
60 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::GaLock => "ga-lock",
64 Self::Production => "production",
65 Self::All => "all",
66 }
67 }
68
69 pub const fn all() -> &'static [Self] {
71 &[Self::GaLock, Self::Production, Self::All]
72 }
73
74 pub const fn supported_cli_profiles() -> &'static [&'static str] {
76 const PROFILE_CLI_NAMES: &[&str] =
77 &["auto", "ga-lock", "ga", "ga_lock", "prod", "production", "all"];
78
79 PROFILE_CLI_NAMES
80 }
81
82 pub const fn aliases(self) -> &'static [&'static str] {
84 match self {
85 Self::GaLock => GA_LOCK_ALIASES,
86 Self::Production => PRODUCTION_ALIASES,
87 Self::All => ALL_ALIASES,
88 }
89 }
90}
91
92pub const FEATURE_PROFILE_SPECS: &[FeatureProfileSpec] = &[
94 FeatureProfileSpec {
95 canonical: "ga-lock",
96 aliases: GA_LOCK_ALIASES,
97 description: "Conservative GA-lock profile for minimal runtime surface.",
98 },
99 FeatureProfileSpec {
100 canonical: "production",
101 aliases: PRODUCTION_ALIASES,
102 description: "Production profile for normal runtime feature set.",
103 },
104 FeatureProfileSpec {
105 canonical: "all",
106 aliases: ALL_ALIASES,
107 description: "All in-tree features enabled for snapshot and testing.",
108 },
109];
110
111pub const fn feature_profile_specs() -> &'static [FeatureProfileSpec] {
113 FEATURE_PROFILE_SPECS
114}
115
116#[allow(dead_code, clippy::all, missing_docs)]
118pub mod catalog {
119 include!(concat!(env!("OUT_DIR"), "/feature_contracts.rs"));
120}
121
122#[derive(Debug, Clone, Serialize)]
124pub struct BddFeatureRow {
125 pub id: &'static str,
127 pub spec: &'static str,
129 pub area: &'static str,
131 pub maturity: &'static str,
133 pub advertised: bool,
135 pub counts_in_coverage: bool,
137 pub description: &'static str,
139 pub tests: &'static [&'static str],
141}
142
143pub use catalog::{
144 Feature, LSP_VERSION, VERSION, advertised_features, compliance_percent, has_feature,
145};
146
147pub fn all_features() -> &'static [Feature] {
149 catalog::ALL_FEATURES
150}
151
152pub fn bdd_feature_rows() -> Vec<BddFeatureRow> {
154 let mut rows = all_features()
155 .iter()
156 .map(|feature| BddFeatureRow {
157 id: feature.id,
158 spec: feature.spec,
159 area: feature.area,
160 maturity: feature.maturity,
161 advertised: feature.advertised,
162 counts_in_coverage: feature.counts_in_coverage,
163 description: feature.description,
164 tests: feature.tests,
165 })
166 .collect::<Vec<_>>();
167
168 rows.sort_by(|a, b| a.area.cmp(b.area).then(a.id.cmp(b.id)));
169 rows
170}
171
172pub fn trackable_feature_count_for_grid() -> usize {
174 all_features()
175 .iter()
176 .filter(|feature| feature.maturity != "planned" && feature.counts_in_coverage)
177 .count()
178}
179
180pub fn advertised_trackable_feature_count_for_grid() -> usize {
182 all_features()
183 .iter()
184 .filter(|feature| {
185 feature.maturity != "planned" && feature.counts_in_coverage && feature.advertised
186 })
187 .count()
188}
189
190pub fn compliance_percent_for_grid() -> f32 {
192 let trackable = trackable_feature_count_for_grid();
193 if trackable == 0 {
194 return 0.0;
195 }
196 let advertised = advertised_trackable_feature_count_for_grid();
197 (advertised as f64 / trackable as f64 * 100.0).round() as f32
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
207 fn from_ga_lock_enabled_true_yields_ga_lock() {
208 assert_eq!(FeatureProfileKind::from_ga_lock_enabled(true), FeatureProfileKind::GaLock);
209 }
210
211 #[test]
212 fn from_ga_lock_enabled_false_yields_production() {
213 assert_eq!(FeatureProfileKind::from_ga_lock_enabled(false), FeatureProfileKind::Production,);
214 }
215
216 #[test]
217 fn all_profiles_returns_three_variants() {
218 let all = FeatureProfileKind::all();
219 assert_eq!(all.len(), 3);
220 assert_eq!(all[0], FeatureProfileKind::GaLock);
221 assert_eq!(all[1], FeatureProfileKind::Production);
222 assert_eq!(all[2], FeatureProfileKind::All);
223 }
224
225 #[test]
226 fn from_str_name_rejects_unknown_token() {
227 assert!(FeatureProfileKind::from_str_name("bogus").is_none());
228 assert!(FeatureProfileKind::from_str_name("").is_none());
229 assert!(FeatureProfileKind::from_str_name("GA-LOCK").is_none());
230 }
231
232 #[test]
233 fn aliases_are_non_empty_for_every_profile() {
234 for profile in FeatureProfileKind::all() {
235 assert!(
236 !profile.aliases().is_empty(),
237 "aliases for {} should not be empty",
238 profile.as_str()
239 );
240 }
241 }
242
243 #[test]
244 fn aliases_contain_canonical_name() {
245 for profile in FeatureProfileKind::all() {
246 let aliases = profile.aliases();
247 assert!(
248 aliases.contains(&profile.as_str()),
249 "aliases for {} should contain canonical name",
250 profile.as_str()
251 );
252 }
253 }
254
255 #[test]
256 fn supported_cli_profiles_covers_all_aliases() {
257 let cli_tokens = FeatureProfileKind::supported_cli_profiles();
258 for profile in FeatureProfileKind::all() {
259 for alias in profile.aliases() {
260 assert!(
261 cli_tokens.contains(alias),
262 "CLI tokens should include alias '{}' for profile '{}'",
263 alias,
264 profile.as_str()
265 );
266 }
267 }
268 }
269
270 #[test]
271 fn auto_token_resolves_to_current() {
272 let resolved = FeatureProfileKind::from_str_name("auto");
273 assert_eq!(resolved, Some(FeatureProfileKind::current()));
274 }
275
276 #[test]
279 fn feature_profile_specs_has_three_entries() {
280 let specs = feature_profile_specs();
281 assert_eq!(specs.len(), 3);
282 }
283
284 #[test]
285 fn feature_profile_specs_canonical_names_match_enum() {
286 let specs = feature_profile_specs();
287 let expected_names: Vec<&str> =
288 FeatureProfileKind::all().iter().map(|p| p.as_str()).collect();
289 let spec_names: Vec<&str> = specs.iter().map(|s| s.canonical).collect();
290 assert_eq!(spec_names, expected_names);
291 }
292
293 #[test]
294 fn feature_profile_specs_descriptions_are_non_empty() {
295 for spec in feature_profile_specs() {
296 assert!(
297 !spec.description.is_empty(),
298 "description for '{}' should not be empty",
299 spec.canonical
300 );
301 }
302 }
303
304 #[test]
307 fn all_features_is_non_empty() {
308 assert!(!all_features().is_empty());
309 }
310
311 #[test]
312 fn all_features_have_non_empty_ids() {
313 for feature in all_features() {
314 assert!(!feature.id.is_empty(), "feature id should not be empty");
315 }
316 }
317
318 #[test]
319 fn all_features_have_valid_areas() {
320 let valid_areas = ["text_document", "workspace", "window", "notebook", "debug", "protocol"];
321 for feature in all_features() {
322 assert!(
323 valid_areas.contains(&feature.area),
324 "feature '{}' has unexpected area '{}'",
325 feature.id,
326 feature.area
327 );
328 }
329 }
330
331 #[test]
332 fn all_features_have_valid_maturity() {
333 let valid_maturities = ["ga", "beta", "alpha", "planned"];
334 for feature in all_features() {
335 assert!(
336 valid_maturities.contains(&feature.maturity),
337 "feature '{}' has unexpected maturity '{}'",
338 feature.id,
339 feature.maturity
340 );
341 }
342 }
343
344 #[test]
345 fn feature_ids_are_unique() {
346 let ids: Vec<&str> = all_features().iter().map(|f| f.id).collect();
347 let mut deduped = ids.clone();
348 deduped.sort_unstable();
349 deduped.dedup();
350 assert_eq!(ids.len(), deduped.len(), "feature IDs must be unique");
351 }
352
353 #[test]
354 fn bdd_feature_rows_sorted_by_area_then_id() {
355 let rows = bdd_feature_rows();
356 for window in rows.windows(2) {
357 let ordering = window[0].area.cmp(window[1].area).then(window[0].id.cmp(window[1].id));
358 assert!(
359 ordering.is_le(),
360 "BDD rows not sorted: '{}' in '{}' should come before '{}' in '{}'",
361 window[0].id,
362 window[0].area,
363 window[1].id,
364 window[1].area,
365 );
366 }
367 }
368
369 #[test]
370 fn bdd_feature_rows_count_matches_all_features() {
371 assert_eq!(bdd_feature_rows().len(), all_features().len());
372 }
373
374 #[test]
375 fn trackable_features_are_subset_of_all() {
376 let all_count = all_features().len();
377 let trackable = trackable_feature_count_for_grid();
378 assert!(trackable <= all_count);
379 }
380
381 #[test]
382 fn advertised_trackable_is_subset_of_trackable() {
383 let trackable = trackable_feature_count_for_grid();
384 let advertised = advertised_trackable_feature_count_for_grid();
385 assert!(advertised <= trackable);
386 }
387
388 #[test]
389 fn compliance_percent_is_in_valid_range() {
390 let pct = compliance_percent_for_grid();
391 assert!((0.0..=100.0).contains(&pct), "compliance must be 0-100, got {pct}");
392 }
393
394 #[test]
395 fn has_feature_returns_true_for_known_ids() {
396 assert!(has_feature("lsp.completion"));
397 assert!(has_feature("lsp.hover"));
398 assert!(has_feature("lsp.definition"));
399 }
400
401 #[test]
402 fn has_feature_returns_false_for_unknown_ids() {
403 assert!(!has_feature("lsp.nonexistent"));
404 assert!(!has_feature(""));
405 }
406
407 #[test]
408 fn advertised_features_is_non_empty() {
409 assert!(!advertised_features().is_empty());
410 }
411
412 #[test]
413 fn version_strings_are_non_empty() {
414 assert!(!VERSION.is_empty());
415 assert!(!LSP_VERSION.is_empty());
416 }
417}