xcodeproj/pbxproj/
mod.rs

1//! pbxproj file serialize and deserializer
2mod 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/// `Main` Representation of project.pbxproj file
17#[derive(Default, derive_new::new, derive_deref_rs::Deref)]
18pub struct PBXRootObject {
19    /// archiveVersion
20    archive_version: u8,
21    /// objectVersion
22    object_version: u8,
23    /// classes
24    classes: PBXHashMap,
25    /// Objects
26    #[deref]
27    objects: PBXObjectCollection,
28    /// rootObjectReference
29    root_object_reference: String,
30}
31
32impl PBXRootObject {
33    /// Get the pbxproject's archive version.
34    #[must_use]
35    pub fn archive_version(&self) -> u8 {
36        self.archive_version
37    }
38
39    /// Get the pbxproject's object version.
40    #[must_use]
41    pub fn object_version(&self) -> u8 {
42        self.object_version
43    }
44
45    /// Get a reference to the pbxproject's classes.
46    #[must_use]
47    pub fn classes(&self) -> &PBXHashMap {
48        &self.classes
49    }
50
51    /// Get a reference to the pbxproject's root object reference.
52    #[must_use]
53    pub fn root_object_reference(&self) -> &str {
54        self.root_object_reference.as_ref()
55    }
56
57    /// Get Root PBXProject
58    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    /// Get root group
67    pub fn root_group(&self) -> PBXFSReference {
68        self.root_project().main_group
69    }
70
71    /// Get a reference to the pbxroot object's objects.
72    #[must_use]
73    pub fn objects(&self) -> &PBXObjectCollection {
74        &self.objects
75    }
76
77    /// Get a mutable reference to the pbxroot object's objects.
78    #[must_use]
79    pub fn objects_mut(&mut self) -> &mut PBXObjectCollection {
80        &mut self.objects
81    }
82
83    /// Get a hashmap of targets and their information
84    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!(
180    //     PBXTargetPlatform::IOS,
181    //     targets[0].platform(project.objects())
182    // );
183    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),   // 12
192            (&PBXBuildPhaseKind::Resources, 3),  // 3
193            (&PBXBuildPhaseKind::Frameworks, 1)  // 1
194        ],
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_project = project.root_project();
204    // println!("{:#?}", root_project.targets[0]);
205
206    let root_group = project.root_group();
207    assert_eq!(17, project.files().len());
208    // println!("{:#?}", root_group.children);
209    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!(
226    //     PBXTargetPlatform::IOS,
227    //     targets[0].platform(project.objects())
228    // );
229    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_project = project.root_project();
249    // println!("{:#?}", root_project.targets[0]);
250
251    let root_group = project.root_group();
252    assert_eq!(17, project.files().len());
253    // println!("{:#?}", root_group.children);
254    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}