1#![warn(missing_docs)]
2use perl_lsp_feature_contracts::advertised_features;
11use perl_lsp_feature_flags::{AdvertisedFeatures, BuildFlags};
12use perl_lsp_feature_profile::{FeatureProfileKind, parse_profile_token};
13
14pub fn from_str_name(s: &str) -> Option<FeatureProfile> {
24 FeatureProfileKind::from_str_name(s).map(FeatureProfile::from_kind)
25}
26
27#[derive(Debug, Clone, Copy, Eq, PartialEq)]
29pub enum FeatureProfile {
30 GaLock,
32 Production,
34 All,
36}
37
38impl FeatureProfile {
39 pub const fn from_kind(profile: FeatureProfileKind) -> Self {
41 match profile {
42 FeatureProfileKind::GaLock => Self::GaLock,
43 FeatureProfileKind::Production => Self::Production,
44 FeatureProfileKind::All => Self::All,
45 }
46 }
47
48 pub const fn from_ga_lock_enabled(ga_lock_enabled: bool) -> Self {
50 Self::from_kind(FeatureProfileKind::from_ga_lock_enabled(ga_lock_enabled))
51 }
52
53 pub const fn current() -> Self {
59 Self::from_kind(FeatureProfileKind::current())
60 }
61
62 pub fn from_cli_argument(raw_profile: &str) -> Self {
67 parse_profile_token(raw_profile).map(Self::from_kind).unwrap_or_else(Self::current)
68 }
69
70 pub fn parse_profile(raw_profile: &str) -> Option<Self> {
72 parse_profile_token(raw_profile).map(Self::from_kind)
73 }
74
75 pub fn build_flags(self) -> BuildFlags {
77 match self {
78 Self::GaLock => BuildFlags::ga_lock(),
79 Self::Production => BuildFlags::production(),
80 Self::All => BuildFlags::all(),
81 }
82 }
83
84 pub fn runtime_flags(self, has_perltidy: bool) -> BuildFlags {
87 let mut flags = self.build_flags();
88
89 if has_perltidy {
90 flags.formatting = true;
91 flags.range_formatting = true;
92 }
93
94 flags
95 }
96
97 pub fn advertised_features(self) -> AdvertisedFeatures {
99 self.build_flags().to_advertised_features()
100 }
101
102 pub fn runtime_advertised_features(self, has_perltidy: bool) -> AdvertisedFeatures {
104 self.runtime_flags(has_perltidy).to_advertised_features()
105 }
106
107 pub const fn as_str(self) -> &'static str {
109 match self {
110 Self::GaLock => FeatureProfileKind::GaLock.as_str(),
111 Self::Production => FeatureProfileKind::Production.as_str(),
112 Self::All => FeatureProfileKind::All.as_str(),
113 }
114 }
115
116 pub const fn supported_cli_profiles() -> &'static [&'static str] {
118 perl_lsp_feature_profile::supported_cli_profiles()
119 }
120
121 pub const fn all() -> &'static [Self] {
123 &[Self::GaLock, Self::Production, Self::All]
124 }
125}
126
127pub fn flags_for_profile(profile: FeatureProfile) -> BuildFlags {
129 profile.build_flags()
130}
131
132pub fn flags_for_runtime(profile: FeatureProfile, has_perltidy: bool) -> BuildFlags {
135 profile.runtime_flags(has_perltidy)
136}
137
138pub fn feature_ids_from_flags(flags: &BuildFlags) -> Vec<&'static str> {
140 flags.to_feature_ids()
141}
142
143pub fn catalog_advertised_feature_ids(profile: FeatureProfile) -> Vec<&'static str> {
146 let catalog_ids = advertised_features();
147 let mut ids = feature_ids_from_flags(&flags_for_profile(profile));
148
149 ids.retain(|id| catalog_ids.contains(id));
150 ids
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn profile_labels_are_stable() {
159 assert_eq!(FeatureProfile::GaLock.as_str(), "ga-lock");
160 assert_eq!(FeatureProfile::Production.as_str(), "production");
161 assert_eq!(FeatureProfile::All.as_str(), "all");
162 }
163
164 #[test]
165 fn supported_cli_profiles_contains_expected_values() {
166 let supported = FeatureProfile::supported_cli_profiles();
167 assert!(supported.contains(&"auto"));
168 assert!(supported.contains(&"ga"));
169 assert!(supported.contains(&"ga_lock"));
170 assert!(supported.contains(&"ga-lock"));
171 assert!(supported.contains(&"prod"));
172 assert!(supported.contains(&"production"));
173 assert!(supported.contains(&"all"));
174 }
175
176 #[test]
179 fn from_kind_preserves_all_variants() {
180 assert_eq!(FeatureProfile::from_kind(FeatureProfileKind::GaLock), FeatureProfile::GaLock,);
181 assert_eq!(
182 FeatureProfile::from_kind(FeatureProfileKind::Production),
183 FeatureProfile::Production,
184 );
185 assert_eq!(FeatureProfile::from_kind(FeatureProfileKind::All), FeatureProfile::All,);
186 }
187
188 #[test]
191 fn from_ga_lock_enabled_true_is_ga_lock() {
192 assert_eq!(FeatureProfile::from_ga_lock_enabled(true), FeatureProfile::GaLock);
193 }
194
195 #[test]
196 fn from_ga_lock_enabled_false_is_production() {
197 assert_eq!(FeatureProfile::from_ga_lock_enabled(false), FeatureProfile::Production);
198 }
199
200 #[test]
203 fn from_cli_argument_resolves_known_tokens() {
204 assert_eq!(FeatureProfile::from_cli_argument("ga-lock"), FeatureProfile::GaLock);
205 assert_eq!(FeatureProfile::from_cli_argument(" Prod "), FeatureProfile::Production);
206 assert_eq!(FeatureProfile::from_cli_argument("all"), FeatureProfile::All);
207 }
208
209 #[test]
210 fn from_cli_argument_falls_back_for_unknown() {
211 let result = FeatureProfile::from_cli_argument("bogus");
212 assert_eq!(result, FeatureProfile::current());
213 }
214
215 #[test]
218 fn parse_profile_returns_none_for_unknown() {
219 assert!(FeatureProfile::parse_profile("nope").is_none());
220 }
221
222 #[test]
223 fn parse_profile_returns_some_for_valid() {
224 assert_eq!(FeatureProfile::parse_profile("all"), Some(FeatureProfile::All));
225 assert_eq!(FeatureProfile::parse_profile(" GA_LOCK "), Some(FeatureProfile::GaLock));
226 }
227
228 #[test]
231 fn build_flags_returns_ga_lock_for_ga_lock_profile() {
232 let flags = FeatureProfile::GaLock.build_flags();
233 let expected = BuildFlags::ga_lock();
234 assert_eq!(flags, expected);
235 }
236
237 #[test]
238 fn build_flags_returns_production_for_production_profile() {
239 let flags = FeatureProfile::Production.build_flags();
240 let expected = BuildFlags::production();
241 assert_eq!(flags, expected);
242 }
243
244 #[test]
245 fn build_flags_returns_all_for_all_profile() {
246 let flags = FeatureProfile::All.build_flags();
247 let expected = BuildFlags::all();
248 assert_eq!(flags, expected);
249 }
250
251 #[test]
254 fn runtime_flags_enables_formatting_when_perltidy_available() {
255 let flags = FeatureProfile::Production.runtime_flags(true);
256 assert!(flags.formatting, "formatting should be enabled with perltidy");
257 assert!(flags.range_formatting, "range_formatting should be enabled with perltidy");
258 }
259
260 #[test]
261 fn runtime_flags_preserves_disabled_formatting_without_perltidy() {
262 let flags = FeatureProfile::Production.runtime_flags(false);
263 assert!(!flags.formatting, "formatting should remain off without perltidy");
264 assert!(!flags.range_formatting, "range_formatting should remain off without perltidy");
265 }
266
267 #[test]
270 fn flags_for_profile_matches_build_flags() {
271 for profile in FeatureProfile::all() {
272 assert_eq!(
273 flags_for_profile(*profile),
274 profile.build_flags(),
275 "flags_for_profile({}) should match build_flags()",
276 profile.as_str(),
277 );
278 }
279 }
280
281 #[test]
282 fn flags_for_runtime_matches_runtime_flags() {
283 for &has_perltidy in &[true, false] {
284 for profile in FeatureProfile::all() {
285 assert_eq!(
286 flags_for_runtime(*profile, has_perltidy),
287 profile.runtime_flags(has_perltidy),
288 );
289 }
290 }
291 }
292
293 #[test]
296 fn advertised_features_reflects_build_flags() {
297 let adv = FeatureProfile::Production.advertised_features();
298 assert!(adv.completion);
299 assert!(adv.hover);
300 assert!(!adv.formatting, "production does not advertise formatting without perltidy");
301 }
302
303 #[test]
304 fn runtime_advertised_features_with_perltidy() {
305 let adv = FeatureProfile::Production.runtime_advertised_features(true);
306 assert!(adv.formatting, "production should advertise formatting with perltidy");
307 }
308
309 #[test]
312 fn catalog_advertised_ids_are_non_empty_for_all_profiles() {
313 for profile in FeatureProfile::all() {
314 let ids = catalog_advertised_feature_ids(*profile);
315 assert!(
316 !ids.is_empty(),
317 "catalog_advertised_feature_ids({}) should not be empty",
318 profile.as_str(),
319 );
320 }
321 }
322
323 #[test]
324 fn catalog_advertised_ids_all_superset_of_ga_lock() {
325 let all_ids = catalog_advertised_feature_ids(FeatureProfile::All);
326 let ga_ids = catalog_advertised_feature_ids(FeatureProfile::GaLock);
327 for id in &ga_ids {
328 assert!(all_ids.contains(id), "'all' advertised IDs should contain ga-lock ID '{id}'");
329 }
330 }
331
332 #[test]
333 fn catalog_advertised_ids_only_contain_catalog_known_ids() {
334 let catalog_ids = advertised_features();
335 for profile in FeatureProfile::all() {
336 let ids = catalog_advertised_feature_ids(*profile);
337 for id in &ids {
338 assert!(
339 catalog_ids.contains(id),
340 "profile '{}' emitted non-catalog ID '{id}'",
341 profile.as_str(),
342 );
343 }
344 }
345 }
346
347 #[test]
350 fn all_profiles_returns_three() {
351 assert_eq!(FeatureProfile::all().len(), 3);
352 }
353
354 #[test]
357 fn feature_ids_from_flags_for_default_is_empty() {
358 let ids = feature_ids_from_flags(&BuildFlags::default());
359 assert!(ids.is_empty());
360 }
361
362 #[test]
365 fn feature_ids_from_flags_partial_enables_only_selected() {
366 let flags = BuildFlags { completion: true, hover: true, ..Default::default() };
367 let ids = feature_ids_from_flags(&flags);
368 assert!(ids.contains(&"lsp.completion"));
369 assert!(ids.contains(&"lsp.hover"));
370 assert!(!ids.contains(&"lsp.definition"));
371 assert!(!ids.contains(&"lsp.references"));
372 assert_eq!(ids.len(), 2, "should contain exactly 2 feature IDs");
373 }
374
375 #[test]
376 fn feature_ids_from_flags_single_flag_produces_one_id() {
377 let flags = BuildFlags { rename: true, ..Default::default() };
378 let ids = feature_ids_from_flags(&flags);
379 assert_eq!(ids.len(), 1);
380 assert_eq!(ids[0], "lsp.rename");
381 }
382
383 #[test]
386 fn ga_lock_profile_gates_inline_values_out() {
387 let flags = FeatureProfile::GaLock.build_flags();
388 assert!(!flags.inline_values, "ga-lock must gate out inline_values");
389 }
390
391 #[test]
392 fn production_profile_gates_formatting_out() {
393 let flags = FeatureProfile::Production.build_flags();
394 assert!(!flags.formatting, "production must gate out formatting");
395 assert!(!flags.range_formatting, "production must gate out range_formatting");
396 }
397
398 #[test]
399 fn all_profile_gates_nothing_out() {
400 let flags = FeatureProfile::All.build_flags();
401 assert!(flags.formatting, "all must include formatting");
402 assert!(flags.range_formatting, "all must include range_formatting");
403 assert!(flags.inline_values, "all must include inline_values");
404 }
405
406 #[test]
407 fn all_profile_is_strict_superset_of_ga_lock() {
408 let all_ids = feature_ids_from_flags(&FeatureProfile::All.build_flags());
409 let ga_ids = feature_ids_from_flags(&FeatureProfile::GaLock.build_flags());
410 for id in &ga_ids {
411 assert!(all_ids.contains(id), "'all' must contain ga-lock feature '{id}'");
412 }
413 assert!(
414 all_ids.len() > ga_ids.len(),
415 "'all' should have strictly more features than ga-lock"
416 );
417 }
418
419 #[test]
420 fn all_profile_is_superset_of_production() {
421 let all_ids = feature_ids_from_flags(&FeatureProfile::All.build_flags());
422 let prod_ids = feature_ids_from_flags(&FeatureProfile::Production.build_flags());
423 for id in &prod_ids {
424 assert!(all_ids.contains(id), "'all' must contain production feature '{id}'");
425 }
426 }
427
428 #[test]
431 fn catalog_advertised_ids_are_sorted_for_all_profiles() {
432 for profile in FeatureProfile::all() {
433 let ids = catalog_advertised_feature_ids(*profile);
434 let mut sorted = ids.clone();
435 sorted.sort_unstable();
436 assert_eq!(
437 ids,
438 sorted,
439 "catalog_advertised_feature_ids for {} should be sorted",
440 profile.as_str()
441 );
442 }
443 }
444
445 #[test]
446 fn catalog_advertised_ids_ga_lock_and_production_overlap_on_core_features() {
447 let prod_ids = catalog_advertised_feature_ids(FeatureProfile::Production);
448 let ga_ids = catalog_advertised_feature_ids(FeatureProfile::GaLock);
449 let core_features = ["lsp.completion", "lsp.hover", "lsp.definition", "lsp.references"];
451 for id in &core_features {
452 assert!(prod_ids.contains(id), "production should contain core feature '{id}'");
453 assert!(ga_ids.contains(id), "ga-lock should contain core feature '{id}'");
454 }
455 }
456
457 #[test]
458 fn catalog_advertised_ids_ga_lock_includes_formatting_production_does_not() {
459 let prod_ids = catalog_advertised_feature_ids(FeatureProfile::Production);
460 let ga_ids = catalog_advertised_feature_ids(FeatureProfile::GaLock);
461 assert!(ga_ids.contains(&"lsp.formatting"), "ga-lock should include formatting");
463 assert!(
464 !prod_ids.contains(&"lsp.formatting"),
465 "production should not include formatting (requires perltidy)"
466 );
467 }
468
469 #[test]
472 fn runtime_flags_perltidy_enables_formatting_for_all_profiles() {
473 for profile in FeatureProfile::all() {
474 let flags = profile.runtime_flags(true);
475 assert!(
476 flags.formatting,
477 "runtime with perltidy should enable formatting for {}",
478 profile.as_str()
479 );
480 assert!(
481 flags.range_formatting,
482 "runtime with perltidy should enable range_formatting for {}",
483 profile.as_str()
484 );
485 }
486 }
487
488 #[test]
489 fn runtime_flags_no_perltidy_matches_base_for_production() {
490 let base = FeatureProfile::Production.build_flags();
491 let runtime = FeatureProfile::Production.runtime_flags(false);
492 assert_eq!(base, runtime, "runtime(false) should match build_flags for production");
493 }
494
495 #[test]
496 fn runtime_advertised_features_without_perltidy_disables_formatting() {
497 let adv = FeatureProfile::Production.runtime_advertised_features(false);
498 assert!(!adv.formatting, "production without perltidy should not advertise formatting");
499 assert!(
500 !adv.range_formatting,
501 "production without perltidy should not advertise range_formatting"
502 );
503 }
504
505 #[test]
506 fn runtime_advertised_features_with_perltidy_enables_formatting() {
507 let adv = FeatureProfile::Production.runtime_advertised_features(true);
508 assert!(adv.formatting, "production with perltidy should advertise formatting");
509 assert!(adv.range_formatting, "production with perltidy should advertise range_formatting");
510 }
511
512 #[test]
513 fn advertised_features_all_profile_enables_everything_without_perltidy() {
514 let adv = FeatureProfile::All.advertised_features();
515 assert!(adv.completion);
516 assert!(adv.hover);
517 assert!(adv.definition);
518 assert!(adv.formatting, "all profile should advertise formatting");
519 assert!(adv.semantic_tokens);
520 }
521
522 #[test]
525 fn current_profile_is_deterministic() {
526 let a = FeatureProfile::current();
527 let b = FeatureProfile::current();
528 assert_eq!(a, b, "current() must be deterministic across calls");
529 }
530
531 #[test]
532 fn current_profile_is_production_or_ga_lock() {
533 let current = FeatureProfile::current();
534 let valid = current == FeatureProfile::Production || current == FeatureProfile::GaLock;
535 assert!(valid, "current() must be Production or GaLock, got {:?}", current);
536 }
537
538 #[test]
539 fn current_profile_enables_core_capabilities() {
540 let flags = FeatureProfile::current().build_flags();
541 assert!(flags.completion);
542 assert!(flags.hover);
543 assert!(flags.definition);
544 assert!(flags.references);
545 assert!(flags.document_symbol);
546 }
547
548 #[test]
551 fn from_str_name_resolves_canonical_names() {
552 assert_eq!(from_str_name("ga-lock"), Some(FeatureProfile::GaLock));
553 assert_eq!(from_str_name("production"), Some(FeatureProfile::Production));
554 assert_eq!(from_str_name("all"), Some(FeatureProfile::All));
555 }
556
557 #[test]
558 fn from_str_name_resolves_aliases() {
559 assert_eq!(from_str_name("ga"), Some(FeatureProfile::GaLock));
560 assert_eq!(from_str_name("ga_lock"), Some(FeatureProfile::GaLock));
561 assert_eq!(from_str_name("prod"), Some(FeatureProfile::Production));
562 }
563
564 #[test]
565 fn from_str_name_resolves_auto_to_current() {
566 assert_eq!(from_str_name("auto"), Some(FeatureProfile::current()));
567 }
568
569 #[test]
570 fn from_str_name_returns_none_for_unknown() {
571 assert!(from_str_name("debug").is_none());
572 assert!(from_str_name("").is_none());
573 assert!(from_str_name("GA-LOCK").is_none());
574 }
575
576 #[test]
579 fn feature_profile_debug_is_human_readable() {
580 let debug_str = format!("{:?}", FeatureProfile::Production);
581 assert!(debug_str.contains("Production"), "Debug output should contain variant name");
582 }
583
584 #[test]
585 fn feature_profile_copy_preserves_equality() {
586 let original = FeatureProfile::All;
587 let copied: FeatureProfile = original;
588 let also_copied: FeatureProfile = original;
589 assert_eq!(original, copied);
590 assert_eq!(copied, also_copied);
591 }
592
593 #[test]
596 fn all_profile_has_most_feature_ids() {
597 let ga_count = feature_ids_from_flags(&FeatureProfile::GaLock.build_flags()).len();
598 let prod_count = feature_ids_from_flags(&FeatureProfile::Production.build_flags()).len();
599 let all_count = feature_ids_from_flags(&FeatureProfile::All.build_flags()).len();
600 assert!(
601 all_count >= prod_count,
602 "all ({all_count}) should have >= features than production ({prod_count})"
603 );
604 assert!(
605 all_count >= ga_count,
606 "all ({all_count}) should have >= features than ga-lock ({ga_count})"
607 );
608 }
609}