1use std::{
2 collections::{HashMap, HashSet},
3 fs::File,
4 hash::Hasher,
5 io::Read,
6 path::{Path, PathBuf},
7};
8
9use anyhow::Context;
10use core::hash::Hash;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use serde_yaml::Value;
13
14use super::{
15 checker::ViolationIdentifier, file_utils::expand_glob, ignored, PackageTodo,
16};
17
18#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
19pub struct Pack {
20 #[serde(skip)]
21 pub yml: PathBuf,
22
23 #[serde(skip)]
24 pub name: String,
25
26 #[serde(skip)]
27 pub relative_path: PathBuf,
28
29 #[serde(
30 default,
31 skip_serializing_if = "Option::is_none",
32 serialize_with = "serialize_checker_setting",
33 deserialize_with = "deserialize_checker_setting"
34 )]
35 pub enforce_dependencies: Option<CheckerSetting>,
36
37 #[serde(
38 default,
39 skip_serializing_if = "Option::is_none",
40 serialize_with = "serialize_checker_setting",
41 deserialize_with = "deserialize_checker_setting"
42 )]
43 pub enforce_privacy: Option<CheckerSetting>,
44
45 #[serde(
46 default,
47 skip_serializing_if = "Option::is_none",
48 serialize_with = "serialize_checker_setting",
49 deserialize_with = "deserialize_checker_setting"
50 )]
51 pub enforce_visibility: Option<CheckerSetting>,
52
53 #[serde(
54 default,
55 skip_serializing_if = "Option::is_none",
56 serialize_with = "serialize_checker_setting",
57 deserialize_with = "deserialize_checker_setting"
58 )]
59 pub enforce_layers: Option<CheckerSetting>,
60
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub owner: Option<String>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub layer: Option<String>,
66
67 #[serde(
68 default,
69 skip_serializing_if = "HashSet::is_empty",
70 serialize_with = "serialize_sorted_hashset_of_strings"
71 )]
72 pub dependencies: HashSet<String>,
73
74 #[serde(
75 default,
76 skip_serializing_if = "HashSet::is_empty",
77 serialize_with = "serialize_sorted_hashset_of_strings"
78 )]
79 pub ignored_dependencies: HashSet<String>,
80
81 #[serde(
82 default,
83 skip_serializing_if = "HashSet::is_empty",
84 serialize_with = "serialize_sorted_hashset_of_strings"
85 )]
86 pub ignored_private_constants: HashSet<String>,
87
88 #[serde(
89 default,
90 skip_serializing_if = "HashSet::is_empty",
91 serialize_with = "serialize_sorted_hashset_of_strings"
92 )]
93 pub private_constants: HashSet<String>,
94
95 #[serde(skip)]
96 pub package_todo: PackageTodo,
97
98 #[serde(
99 default,
100 skip_serializing_if = "Option::is_none",
101 serialize_with = "serialize_sorted_option_hashset_of_strings"
102 )]
103 pub visible_to: Option<HashSet<String>>,
104
105 #[serde(
106 default,
107 skip_serializing_if = "Option::is_none",
108 serialize_with = "serialize_checker_setting",
109 deserialize_with = "deserialize_checker_setting"
110 )]
111 pub enforce_folder_privacy: Option<CheckerSetting>,
112
113 #[serde(
114 default,
115 skip_serializing_if = "Option::is_none",
116 serialize_with = "serialize_checker_setting",
117 deserialize_with = "deserialize_checker_setting"
118 )]
119 pub enforce_folder_visibility: Option<CheckerSetting>, #[serde(skip_serializing_if = "is_default_public_folder")]
122 pub public_folder: Option<PathBuf>,
123
124 #[serde(flatten)]
125 pub client_keys: HashMap<String, Value>,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub enforcement_globs_ignore: Option<Vec<EnforcementGlobsIgnore>>,
129}
130
131impl Hash for Pack {
132 fn hash<H: Hasher>(&self, state: &mut H) {
133 self.name.hash(state);
134 }
135}
136
137#[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize, Clone)]
138pub struct EnforcementGlobsIgnore {
139 #[serde(
140 default,
141 serialize_with = "serialize_sorted_hashset_of_strings",
142 skip_serializing_if = "HashSet::is_empty"
143 )]
144 pub enforcements: HashSet<String>,
145
146 #[serde(
147 default,
148 serialize_with = "serialize_sorted_hashset_of_strings",
149 skip_serializing_if = "HashSet::is_empty"
150 )]
151 pub ignores: HashSet<String>,
152
153 #[serde(default)]
154 pub reason: String,
155}
156
157#[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize, Clone)]
158pub enum CheckerSetting {
159 #[default]
160 False,
161 True,
162 Strict,
163}
164
165impl CheckerSetting {
166 pub fn is_false(&self) -> bool {
167 matches!(self, Self::False)
168 }
169
170 pub fn is_strict(&self) -> bool {
171 matches!(self, Self::Strict)
172 }
173}
174
175impl Pack {
176 pub fn last_name(&self) -> &str {
177 self.name.split('/').last().unwrap()
178 }
179
180 pub fn all_violations(&self) -> Vec<ViolationIdentifier> {
181 let mut violations = Vec::new();
182 let violations_by_pack = &self.package_todo.violations_by_defining_pack;
183 for (defining_pack_name, violation_groups) in violations_by_pack {
184 for (constant_name, violation_group) in violation_groups {
185 for violation_type in &violation_group.violation_types {
186 for file in &violation_group.files {
187 let identifier = ViolationIdentifier {
188 violation_type: violation_type.clone(),
189 strict: false,
190 file: file.clone(),
191 constant_name: constant_name.clone(),
192 referencing_pack_name: self.name.clone(),
193 defining_pack_name: defining_pack_name.clone(),
194 };
195
196 violations.push(identifier);
197 }
198 }
199 }
200 }
201 violations
202 }
203
204 pub fn from_path(
205 package_yml_absolute_path: &Path,
206 absolute_root: &Path,
207 ) -> anyhow::Result<Pack> {
208 let mut yaml_contents = String::new();
209
210 let mut file = File::open(package_yml_absolute_path).map_err(|e| {
211 anyhow::Error::new(e).context(format!(
212 "Failed to open the YAML file at {:?}",
213 package_yml_absolute_path
214 ))
215 })?;
216
217 file.read_to_string(&mut yaml_contents).map_err(|e| {
218 anyhow::Error::new(e).context(format!(
219 "Failed to read the YAML file at {:?}",
220 package_yml_absolute_path
221 ))
222 })?;
223
224 let absolute_path_to_package_todo = package_yml_absolute_path
225 .parent()
226 .unwrap()
227 .join("package_todo.yml");
228
229 let package_todo: PackageTodo = if absolute_path_to_package_todo
230 .exists()
231 {
232 let mut package_todo_contents = String::new();
233 let mut file = File::open(&absolute_path_to_package_todo)
234 .context("Failed to open the package_todo.yml file")?;
235 file.read_to_string(&mut package_todo_contents)
236 .context("Could not read the package_todo.yml file")?;
237 serde_yaml::from_str(&package_todo_contents).with_context(|| {
238 format!(
239 "Failed to deserialize the package_todo.yml file at {}. Try deleting the file and running the `update` command to regenerate it.",
240 absolute_path_to_package_todo.display()
241 )
242 })?
243 } else {
244 PackageTodo::default()
245 };
246
247 Pack::from_contents(
248 package_yml_absolute_path,
249 absolute_root,
250 &yaml_contents,
251 package_todo,
252 )
253 }
254
255 pub fn from_contents(
256 package_yml_absolute_path: &Path,
257 absolute_root: &Path,
258 package_yml_contents: &str,
259 package_todo: PackageTodo,
260 ) -> anyhow::Result<Pack> {
261 let pack_result = serde_yaml::from_str(package_yml_contents);
262 let pack = match pack_result {
263 Ok(pack) => pack,
264 Err(e) => {
265 anyhow::bail!(
266 "Failed to deserialize the YAML at {:?} with error: {:?}",
267 package_yml_absolute_path,
268 e
269 )
270 }
271 };
272
273 let package_yml_relative_path = package_yml_absolute_path
274 .strip_prefix(absolute_root)
275 .unwrap();
276 let mut relative_path = package_yml_relative_path
277 .parent()
278 .context("Expected package to be in a parent directory")?
279 .to_owned();
280
281 let mut name = relative_path
282 .to_str()
283 .context("Non-unicode characters?")?
284 .to_owned();
285 let yml = package_yml_absolute_path;
286
287 if name == *"" {
289 name = String::from(".");
290 relative_path = PathBuf::from(".");
291 };
292
293 let pack: Pack = Pack {
294 yml: yml.to_path_buf(),
295 name,
296 relative_path,
297 package_todo,
298 ..pack
299 };
300
301 Ok(pack)
302 }
303
304 pub fn default_autoload_roots(&self) -> Vec<PathBuf> {
305 let root_pattern = self.yml.parent().unwrap().join("app").join("*");
306 let concerns_pattern = root_pattern.join("concerns");
307 let mut roots = expand_glob(root_pattern.to_str().unwrap());
308 roots.extend(expand_glob(concerns_pattern.to_str().unwrap()));
309
310 roots
311 }
312
313 pub fn relative_yml(&self) -> PathBuf {
314 self.relative_path.join("package.yml")
315 }
316
317 pub(crate) fn enforce_folder_privacy(&self) -> &CheckerSetting {
318 if self.enforce_folder_privacy.is_none() {
319 match &self.enforce_folder_visibility {
321 Some(setting) => setting,
322 None => &CheckerSetting::False,
323 }
324 } else {
325 match &self.enforce_folder_privacy {
326 Some(setting) => setting,
327 None => &CheckerSetting::False,
328 }
329 }
330 }
331
332 pub(crate) fn public_folder(&self) -> PathBuf {
333 match &self.public_folder {
334 Some(folder) => folder.to_owned(),
335 None => self.relative_path.join("app/public"),
336 }
337 }
338
339 pub(crate) fn add_dependency(&self, to_pack: &Pack) -> Pack {
340 let mut new_pack = self.clone();
341 new_pack.dependencies.insert(to_pack.name.clone());
342 new_pack
343 }
344
345 pub(crate) fn ignores_for_enforcement(
346 &self,
347 enforcement: &str,
348 ) -> Option<&HashSet<String>> {
349 self.enforcement_globs_ignore.as_ref().and_then(|ignores| {
350 ignores
351 .iter()
352 .find(|ignore| ignore.enforcements.contains(enforcement))
353 .map(|ignore| &ignore.ignores)
354 })
355 }
356
357 pub(crate) fn is_ignored(
358 &self,
359 file_path: &str,
360 enforcement: &str,
361 ) -> anyhow::Result<bool> {
362 if let Some(ignore_rules) = self.ignores_for_enforcement(enforcement) {
363 return ignored::is_ignored(ignore_rules, file_path);
364 }
365 Ok(false)
366 }
367}
368
369fn serialize_sorted_hashset_of_strings<S>(
370 value: &HashSet<String>,
371 serializer: S,
372) -> Result<S::Ok, S::Error>
373where
374 S: Serializer,
375{
376 let mut value: Vec<&String> = value.iter().collect();
378 value.sort();
379 value.serialize(serializer)
380}
381
382fn serialize_sorted_option_hashset_of_strings<S>(
383 value: &Option<HashSet<String>>,
384 serializer: S,
385) -> Result<S::Ok, S::Error>
386where
387 S: Serializer,
388{
389 match value {
390 Some(value) => serialize_sorted_hashset_of_strings(value, serializer),
391 None => serializer.serialize_none(),
392 }
393}
394
395fn is_default_public_folder(value: &Option<PathBuf>) -> bool {
396 match value {
397 Some(value) => value == &PathBuf::from("app/public"),
398 None => true,
399 }
400}
401
402const KEY_SORT_ORDER: &[&str] = &[
403 "enforce_dependencies",
404 "enforce_privacy",
405 "enforce_layers",
406 "enforce_visibility",
407 "enforce_folder_privacy",
408 "enforce_folder_visibility",
409 "enforce_architecture",
410 "layer",
411 "public_path",
412 "dependencies",
413 "owner",
414 "private_constants",
415 "visible_to",
416 "enforcement_globs_ignore",
417 "metadata",
418];
419
420pub fn serialize_pack(pack: &Pack) -> String {
421 let serialized: Value = serde_yaml::to_value(pack).unwrap();
422 let mapping = serialized.as_mapping().unwrap();
423
424 let mut ordered_map: Vec<(String, Value)> = Vec::new();
426
427 for key in KEY_SORT_ORDER {
429 if let Some(value) = mapping.get(Value::String(key.to_string())) {
430 ordered_map.push((key.to_string(), value.clone()));
431 }
432 }
433
434 let mut added_keys: HashSet<String> =
436 ordered_map.iter().map(|(k, _)| k.clone()).collect();
437 for (key, value) in mapping {
438 if let Value::String(key_str) = key {
439 if !added_keys.contains(key_str) {
440 ordered_map.push((key_str.clone(), value.clone()));
441 added_keys.insert(key_str.clone());
442 }
443 }
444 }
445
446 let mut sorted_mapping = serde_yaml::Mapping::new();
448 for (key, value) in ordered_map {
449 sorted_mapping.insert(Value::String(key), value);
450 }
451
452 let raw_yaml =
454 serde_yaml::to_string(&Value::Mapping(sorted_mapping)).unwrap();
455
456 let trimmed_yaml = raw_yaml.trim_start_matches("---\n").to_string();
458
459 if trimmed_yaml == "{}\n" {
460 "".to_string()
461 } else {
462 trimmed_yaml
463 }
464}
465
466pub fn write_pack_to_disk(pack: &Pack) -> anyhow::Result<()> {
467 let serialized_pack = serialize_pack(pack);
468 let pack_dir = pack.yml.parent().ok_or_else(|| {
469 anyhow::Error::new(std::io::Error::new(
470 std::io::ErrorKind::NotFound,
471 format!("Failed to get parent directory of pack {:?}", &pack.yml),
472 ))
473 })?;
474
475 std::fs::create_dir_all(pack_dir).map_err(|e| {
476 anyhow::Error::new(e).context(format!(
477 "Failed to create directory for pack {:?}",
478 &pack_dir
479 ))
480 })?;
481
482 std::fs::write(&pack.yml, serialized_pack).map_err(|e| {
483 anyhow::Error::new(e)
484 .context(format!("Failed to write pack to disk {:?}", &pack.yml))
485 })?;
486
487 Ok(())
488}
489
490fn serialize_checker_setting<S>(
491 value: &Option<CheckerSetting>,
492 serializer: S,
493) -> Result<S::Ok, S::Error>
494where
495 S: Serializer,
496{
497 match value {
498 Some(CheckerSetting::False) => serializer.serialize_bool(false),
499 Some(CheckerSetting::True) => serializer.serialize_bool(true),
500 Some(CheckerSetting::Strict) => serializer.serialize_str("strict"),
501 None => serializer.serialize_none(),
502 }
503}
504
505fn deserialize_checker_setting<'de, D>(
506 deserializer: D,
507) -> Result<Option<CheckerSetting>, D::Error>
508where
509 D: Deserializer<'de>,
510{
511 let s = String::deserialize(deserializer);
513
514 match s.unwrap().as_str() {
515 "false" => Ok(Some(CheckerSetting::False)),
516 "true" => Ok(Some(CheckerSetting::True)),
517 "strict" => Ok(Some(CheckerSetting::Strict)),
518 _ => Err(serde::de::Error::custom(
519 "expected one of: false, true, strict",
520 )),
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use crate::test_util;
527
528 use super::*;
529 use pretty_assertions::assert_eq;
530
531 fn reserialize_pack(pack_yml: &str) -> String {
532 let deserialized_pack = serde_yaml::from_str::<Pack>(pack_yml).unwrap();
533 serialize_pack(&deserialized_pack)
534 }
535
536 #[test]
537 fn test_serde_sorted_dependencies() {
538 let pack_yml = r#"
539# some comment
540dependencies:
541 - packs/c
542 - packs/a
543 - packs/b
544"#;
545
546 let actual = reserialize_pack(pack_yml);
547
548 let expected = r#"
549dependencies:
550- packs/a
551- packs/b
552- packs/c
553"#
554 .trim_start();
555
556 assert_eq!(expected, actual)
557 }
558
559 #[test]
560 fn test_serde_with_enforcements() {
561 let pack_yml = r#"
562# some comment
563enforce_privacy: true
564enforce_dependencies: strict
565dependencies:
566 - packs/c
567 - packs/a
568 - packs/b
569foobar: true
570"#;
571
572 let actual = reserialize_pack(pack_yml);
573
574 let expected = r#"
575enforce_dependencies: strict
576enforce_privacy: true
577dependencies:
578- packs/a
579- packs/b
580- packs/c
581foobar: true
582"#
583 .trim_start();
584
585 assert_eq!(expected, actual)
586 }
587
588 #[test]
589 fn test_serde_with_arbitrary_client_keys() {
590 let pack_yml = r#"
591# some comment
592dependencies:
593 - packs/c
594 - packs/a
595 - packs/b
596foobar: true
597"#;
598
599 let actual = reserialize_pack(pack_yml);
600
601 let expected = r#"
602dependencies:
603- packs/a
604- packs/b
605- packs/c
606foobar: true
607"#
608 .trim_start();
609
610 assert_eq!(expected, actual)
611 }
612
613 #[test]
614 fn test_serde_with_duplicate_dependencies() {
615 let pack_yml = r#"
616dependencies:
617 - packs/a
618 - packs/b
619 - packs/a
620 - packs/a
621 - packs/a
622"#;
623
624 let actual = reserialize_pack(pack_yml);
625
626 let expected = r#"
627dependencies:
628- packs/a
629- packs/b
630"#
631 .trim_start();
632
633 assert_eq!(expected, actual)
634 }
635
636 #[test]
637 fn test_serde_with_explicitly_empty_visible() {
638 let pack_yml = r#"
639visible_to:
640 - packs/c
641 - packs/a
642 - packs/b
643"#;
644
645 let actual = reserialize_pack(pack_yml);
646
647 let expected = r#"
648visible_to:
649- packs/a
650- packs/b
651- packs/c
652"#
653 .trim_start();
654
655 assert_eq!(expected, actual)
656 }
657
658 #[test]
659 fn test_serde_with_metadata() {
660 let pack_yml = r#"
661enforce_dependencies: false
662metadata:
663 foobar: true
664"#;
665
666 let actual = reserialize_pack(pack_yml);
667
668 let expected = r#"
669enforce_dependencies: false
670metadata:
671 foobar: true
672"#
673 .trim_start();
674
675 assert_eq!(expected, actual)
676 }
677
678 #[test]
679 fn test_serde_with_many_fields() {
680 let pack_yml = r#"
681enforce_dependencies: true
682enforce_privacy: true
683dependencies:
684- packs/utilities
685enforce_architecture: true
686"#;
687
688 let actual = reserialize_pack(pack_yml);
689
690 let expected = r#"
691enforce_dependencies: true
692enforce_privacy: true
693enforce_architecture: true
694dependencies:
695- packs/utilities
696"#
697 .trim_start();
698
699 assert_eq!(expected, actual)
700 }
701
702 #[test]
703 fn test_serde_with_owner() {
704 let pack_yml = r#"
705owner: Foobar
706enforce_dependencies: true
707"#;
708
709 let actual = reserialize_pack(pack_yml);
710
711 let expected = r#"
712enforce_dependencies: true
713owner: Foobar
714"#
715 .trim_start();
716
717 assert_eq!(expected, actual)
718 }
719
720 #[test]
721 fn test_serde_with_enforcement_globs() {
722 let pack_yml = r#"
723enforcement_globs_ignore:
724 - enforcements:
725 - privacy
726 ignores:
727 - "**/*"
728 - "!packs/foo"
729 reason: "deprecated foo"
730 - enforcements:
731 - layer
732 ignores:
733 - packs/bar
734 reason: "deprecated bar"
735 "#
736 .trim_start();
737
738 let pack: Result<Pack, _> = serde_yaml::from_str(pack_yml);
739 let pack = pack.unwrap();
740 assert_eq!(
741 pack.clone().enforcement_globs_ignore.unwrap(),
742 vec![
743 EnforcementGlobsIgnore {
744 enforcements: ["privacy"]
745 .iter()
746 .map(|s| s.to_string())
747 .collect(),
748 ignores: ["**/*", "!packs/foo"]
749 .iter()
750 .map(|s| s.to_string())
751 .collect(),
752 reason: "deprecated foo".to_string(),
753 },
754 EnforcementGlobsIgnore {
755 enforcements: ["layer"]
756 .iter()
757 .map(|s| s.to_string())
758 .collect(),
759 ignores: ["packs/bar"]
760 .iter()
761 .map(|s| s.to_string())
762 .collect(),
763 reason: "deprecated bar".to_string(),
764 },
765 ]
766 );
767
768 let reserialized = reserialize_pack(pack_yml);
769 let re_pack: Result<Pack, _> = serde_yaml::from_str(&reserialized);
770 let re_pack = re_pack.unwrap();
771 assert_eq!(pack, re_pack);
772
773 assert_eq!(
774 pack.ignores_for_enforcement("privacy"),
775 Some(&{
776 ["**/*", "!packs/foo"]
777 .iter()
778 .map(|s| s.to_string())
779 .collect()
780 })
781 );
782 assert_eq!(pack.ignores_for_enforcement("nope"), None);
783 }
784
785 #[test]
786 fn test_serde_with_empty_pack() {
787 let pack_yml = r#""#;
788
789 let actual = reserialize_pack(pack_yml);
790
791 let expected = r#""#.trim_start();
792
793 assert_eq!(expected, actual)
794 }
795
796 #[test]
797 fn test_autoload_roots() {
798 let root = test_util::get_absolute_root(test_util::SIMPLE_APP);
799 let pack =
800 Pack::from_path(root.join("package.yml").as_path(), root.as_path());
801 assert!(pack.is_ok());
802
803 let actual = pack.unwrap().default_autoload_roots();
804 let expected =
805 vec![root.join("app/company_data"), root.join("app/services")];
806 assert_eq!(expected, actual)
807 }
808
809 #[test]
810 fn test_all_recorded_violations() -> anyhow::Result<()> {
811 let root = test_util::get_absolute_root(
812 "tests/fixtures/contains_package_todo",
813 );
814 let pack = Pack::from_path(
815 root.join("packs/foo/package.yml").as_path(),
816 root.as_path(),
817 )?;
818
819 let mut actual = pack.all_violations();
820 actual.sort_by(|a, b| a.file.cmp(&b.file));
821
822 let expected = vec![
823 ViolationIdentifier {
824 violation_type: "dependency".to_string(),
825 strict: false,
826 file: "packs/foo/app/services/foo.rb".to_string(),
827 constant_name: "::Bar".to_string(),
828 referencing_pack_name: "packs/foo".to_string(),
829 defining_pack_name: "packs/bar".to_string(),
830 },
831 ViolationIdentifier {
832 violation_type: "dependency".to_string(),
833 strict: false,
834 file: "packs/foo/app/services/other_foo.rb".to_string(),
835 constant_name: "::Bar".to_string(),
836 referencing_pack_name: "packs/foo".to_string(),
837 defining_pack_name: "packs/bar".to_string(),
838 },
839 ];
840
841 assert_eq!(expected, actual);
842
843 Ok(())
844 }
845}