1mod object;
3mod value;
4
5pub(crate) mod pest;
6pub use object::*;
7pub use value::*;
8
9use anyhow::Result;
10use std::{
11 collections::HashMap,
12 path::{Path, PathBuf},
13};
14use tap::Pipe;
15
16#[derive(Default, derive_new::new, derive_deref_rs::Deref)]
18pub struct PBXRootObject {
19 archive_version: u8,
21 object_version: u8,
23 classes: PBXHashMap,
25 #[deref]
27 objects: PBXObjectCollection,
28 root_object_reference: String,
30}
31
32impl PBXRootObject {
33 #[must_use]
35 pub fn archive_version(&self) -> u8 {
36 self.archive_version
37 }
38
39 #[must_use]
41 pub fn object_version(&self) -> u8 {
42 self.object_version
43 }
44
45 #[must_use]
47 pub fn classes(&self) -> &PBXHashMap {
48 &self.classes
49 }
50
51 #[must_use]
53 pub fn root_object_reference(&self) -> &str {
54 self.root_object_reference.as_ref()
55 }
56
57 pub fn root_project(&self) -> PBXProject {
59 self.objects
60 .projects()
61 .into_iter()
62 .find(|o| o.id == self.root_object_reference())
63 .unwrap()
64 }
65
66 pub fn root_group(&self) -> PBXFSReference {
68 self.root_project().main_group
69 }
70
71 #[must_use]
73 pub fn objects(&self) -> &PBXObjectCollection {
74 &self.objects
75 }
76
77 #[must_use]
79 pub fn objects_mut(&mut self) -> &mut PBXObjectCollection {
80 &mut self.objects
81 }
82
83 pub fn targets_info(&self) -> HashMap<String, PBXTargetInfo> {
85 self.targets()
86 .into_iter()
87 .flat_map(|t| {
88 let name = t.name?.to_string();
89 let info = t.info(&self.objects);
90 Some((name, info))
91 })
92 .collect::<HashMap<_, _>>()
93 }
94}
95
96impl std::fmt::Debug for PBXRootObject {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("PBXRootObject")
99 .field("archive_version", &self.archive_version)
100 .field("object_version", &self.object_version)
101 .field("classes", &self.classes)
102 .field("root_object_reference", &self.root_object_reference)
103 .finish()
104 }
105}
106
107impl TryFrom<PBXHashMap> for PBXRootObject {
108 type Error = anyhow::Error;
109 fn try_from(mut map: PBXHashMap) -> Result<Self> {
110 let archive_version = map.try_remove_number("archiveVersion")? as u8;
111 let object_version = map.try_remove_number("objectVersion")? as u8;
112 let classes = map.try_remove_object("classes").unwrap_or_default();
113 let root_object_reference = map.try_remove_string("rootObject")?;
114 let objects = PBXObjectCollection(
115 map.try_remove_object("objects")?
116 .0
117 .into_iter()
118 .map(|(k, v)| (k, v.try_into_object().unwrap()))
119 .collect(),
120 );
121
122 Ok(Self {
123 archive_version,
124 object_version,
125 classes,
126 objects,
127 root_object_reference,
128 })
129 }
130}
131
132impl TryFrom<&str> for PBXRootObject {
133 type Error = anyhow::Error;
134 fn try_from(content: &str) -> Result<Self> {
135 use crate::pbxproj::pest::PBXProjectParser;
136
137 PBXProjectParser::try_from_str(content)?.pipe(Self::try_from)
138 }
139}
140
141impl TryFrom<String> for PBXRootObject {
142 type Error = anyhow::Error;
143 fn try_from(content: String) -> Result<Self> {
144 Self::try_from(content.as_str())
145 }
146}
147
148impl TryFrom<&Path> for PBXRootObject {
149 type Error = anyhow::Error;
150
151 fn try_from(value: &Path) -> Result<Self> {
152 std::fs::read_to_string(&value)
153 .map_err(|e| anyhow::anyhow!("PBXProjectData from path {value:?}: {e}"))?
154 .pipe(TryFrom::try_from)
155 }
156}
157
158impl TryFrom<PathBuf> for PBXRootObject {
159 type Error = anyhow::Error;
160
161 fn try_from(value: PathBuf) -> Result<Self> {
162 Self::try_from(value.as_path())
163 }
164}
165
166#[test]
167fn test_demo1_representation() {
168 let test_content = include_str!("../../tests/samples/demo1.pbxproj");
169 let project = PBXRootObject::try_from(test_content).unwrap();
170 let targets_info = project.targets_info();
171 println!("{targets_info:#?}");
172 let targets = project.targets();
173
174 assert_eq!(1, targets.len());
175 assert_eq!(&PBXTargetKind::Native, targets[0].kind);
176 assert_eq!(Some(&String::from("Wordle")), targets[0].product_name);
177 assert_eq!(Some(&String::from("Wordle")), targets[0].name);
178 assert_eq!(PBXProductType::Application, targets[0].product_type);
179 assert_eq!(None, targets[0].build_tool_path);
184 assert_eq!(None, targets[0].build_arguments_string);
185 assert_eq!(None, targets[0].build_working_directory);
186 assert_eq!(None, targets[0].pass_build_settings_in_environment);
187 assert_eq!(3, targets[0].build_phases.len());
188
189 assert_eq!(
190 vec![
191 (&PBXBuildPhaseKind::Sources, 12), (&PBXBuildPhaseKind::Resources, 3), (&PBXBuildPhaseKind::Frameworks, 1) ],
195 targets[0]
196 .build_phases
197 .iter()
198 .map(|phase| (&phase.kind, phase.files.len()))
199 .collect::<Vec<_>>()
200 );
201
202 assert_eq!(1, project.projects().len());
203 let root_group = project.root_group();
207 assert_eq!(17, project.files().len());
208 assert_eq!(3, root_group.children.len());
210 assert_eq!(None, root_group.name);
211 assert_eq!(None, root_group.path);
212}
213
214#[test]
215fn test_demo10_representation() {
216 let test_content = include_str!("../../tests/samples/demo10.pbxproj");
217 let project = PBXRootObject::try_from(test_content).unwrap();
218 let targets = project.targets();
219
220 assert_eq!(1, targets.len());
221 assert_eq!(&PBXTargetKind::Native, targets[0].kind);
222 assert_eq!(Some(&String::from("Scrumdinger")), targets[0].product_name);
223 assert_eq!(Some(&String::from("Scrumdinger")), targets[0].name);
224 assert_eq!(PBXProductType::Application, targets[0].product_type);
225 assert_eq!(None, targets[0].build_tool_path);
230 assert_eq!(None, targets[0].build_arguments_string);
231 assert_eq!(None, targets[0].build_working_directory);
232 assert_eq!(None, targets[0].pass_build_settings_in_environment);
233 assert_eq!(3, targets[0].build_phases.len());
234 assert_eq!(
235 vec![
236 (&PBXBuildPhaseKind::Sources, 11),
237 (&PBXBuildPhaseKind::Frameworks, 0),
238 (&PBXBuildPhaseKind::Resources, 4)
239 ],
240 targets[0]
241 .build_phases
242 .iter()
243 .map(|phase| (&phase.kind, phase.files.len()))
244 .collect::<Vec<_>>()
245 );
246
247 assert_eq!(1, project.projects().len());
248 let root_group = project.root_group();
252 assert_eq!(17, project.files().len());
253 assert_eq!(5, root_group.children.len());
255 assert_eq!(None, root_group.name);
256 assert_eq!(None, root_group.path);
257}
258
259#[cfg(test)]
260macro_rules! test_demo_file {
261 ($name:expr) => {{
262 let (root, name) = (env!("CARGO_MANIFEST_DIR"), stringify!($name));
263 let path = format!("{root}/tests/samples/{name}.pbxproj");
264 let file = crate::pbxproj::PBXRootObject::try_from(std::path::PathBuf::from(path));
265 if file.is_err() {
266 println!("Error: {:#?}", file.as_ref().unwrap_err())
267 }
268 assert!(file.is_ok());
269 file.unwrap()
270 }};
271}
272
273#[cfg(test)]
274pub(crate) use test_demo_file;
275
276#[cfg(test)]
277mod tests {
278 macro_rules! test_samples {
279 ($($name:ident),*) => {
280 $(#[test]
281 fn $name() {
282 test_demo_file!($name);
283 })*
284 };
285 }
286
287 test_samples![demo1, demo2, demo3, demo4, demo5, demo6, demo7, demo8, demo9, demo10, demo11];
288}