text_stub_library/
lib.rs

1// Copyright 2022 Gregory Szorc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9pub mod yaml;
10
11use yaml::*;
12use yaml_rust::ScanError;
13
14/// Version of a TBD document.
15#[derive(Copy, Clone, Debug)]
16pub enum TbdVersion {
17    V1,
18    V2,
19    V3,
20    V4,
21}
22
23/// A parsed TBD record from a YAML document.
24///
25/// This is an enum over the raw, versioned YAML data structures.
26pub enum TbdVersionedRecord {
27    V1(TbdVersion1),
28    V2(TbdVersion2),
29    V3(TbdVersion3),
30    V4(TbdVersion4),
31}
32
33/// Represents an error when parsing TBD YAML.
34#[derive(Debug)]
35pub enum ParseError {
36    YamlError(yaml_rust::ScanError),
37    DocumentCountMismatch,
38    Serde(serde_yaml::Error),
39}
40
41impl std::fmt::Display for ParseError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::YamlError(e) => e.fmt(f),
45            Self::DocumentCountMismatch => {
46                f.write_str("mismatch in expected document count when parsing YAML")
47            }
48            Self::Serde(e) => e.fmt(f),
49        }
50    }
51}
52
53impl std::error::Error for ParseError {}
54
55impl From<yaml_rust::ScanError> for ParseError {
56    fn from(e: ScanError) -> Self {
57        Self::YamlError(e)
58    }
59}
60
61impl From<serde_yaml::Error> for ParseError {
62    fn from(e: serde_yaml::Error) -> Self {
63        Self::Serde(e)
64    }
65}
66
67const TBD_V2_DOCUMENT_START: &str = "--- !tapi-tbd-v2";
68const TBD_V3_DOCUMENT_START: &str = "--- !tapi-tbd-v3";
69const TBD_V4_DOCUMENT_START: &str = "--- !tapi-tbd";
70
71/// Parse TBD records from a YAML stream.
72///
73/// Returns a series of parsed records contained in the stream.
74pub fn parse_str(data: &str) -> Result<Vec<TbdVersionedRecord>, ParseError> {
75    // serde_yaml didn't support tags on documents with YAML streams
76    // (https://github.com/dtolnay/serde-yaml/issues/147). This code is left
77    // over from that era.
78    //
79    // Our extremely hacky and inefficient solution is to parse the stream once
80    // using yaml_rust to ensure it is valid YAML. Then we do a manual pass
81    // scanning for document markers (`---` and `...`) and corresponding TBD
82    // tags. We then pair things up and feed each document into the serde_yaml
83    // deserializer for the given type.
84
85    let yamls = yaml_rust::YamlLoader::load_from_str(data)?;
86
87    // We got valid YAML. That's a good sign. Proceed with document/tag scanning.
88
89    let mut document_versions = vec![];
90
91    for line in data.lines() {
92        // Start of new YAML document.
93        if line.starts_with("---") {
94            let version = if line.starts_with(TBD_V2_DOCUMENT_START) {
95                TbdVersion::V2
96            } else if line.starts_with(TBD_V3_DOCUMENT_START) {
97                TbdVersion::V3
98            } else if line.starts_with(TBD_V4_DOCUMENT_START) {
99                TbdVersion::V4
100            } else {
101                // Version 1 has no document tag.
102                TbdVersion::V1
103            };
104
105            document_versions.push(version);
106        }
107    }
108
109    // The initial document marker in a YAML file is optional. And the
110    // `---` marker is a version 1 TBD. So if there is a count mismatch,
111    // insert a version 1 at the beginning of the versions list.
112    if document_versions.len() == yamls.len() - 1 {
113        document_versions.insert(0, TbdVersion::V1);
114    } else if document_versions.len() != yamls.len() {
115        return Err(ParseError::DocumentCountMismatch);
116    }
117
118    let mut res = vec![];
119
120    for (index, value) in yamls.iter().enumerate() {
121        // TODO We could almost certainly avoid the YAML parsing round trip
122        let mut s = String::new();
123        yaml_rust::YamlEmitter::new(&mut s).dump(value).unwrap();
124
125        res.push(match document_versions[index] {
126            TbdVersion::V1 => TbdVersionedRecord::V1(serde_yaml::from_str(&s)?),
127            TbdVersion::V2 => TbdVersionedRecord::V2(serde_yaml::from_str(&s)?),
128            TbdVersion::V3 => TbdVersionedRecord::V3(serde_yaml::from_str(&s)?),
129            TbdVersion::V4 => TbdVersionedRecord::V4(serde_yaml::from_str(&s)?),
130        })
131    }
132
133    Ok(res)
134}
135
136#[cfg(test)]
137mod tests {
138    use {
139        super::*,
140        apple_sdk::{AppleSdk, SdkSearch, SdkSearchLocation, SimpleSdk},
141        rand::seq::SliceRandom,
142        rayon::prelude::*,
143    };
144
145    #[test]
146    fn test_parse_apple_sdk_tbds() {
147        // This will find older Xcode versions and their SDKs when run in GitHub
148        // Actions. That gives us extreme test coverage of real world .tbd files.
149        let sdks = SdkSearch::empty()
150            .location(SdkSearchLocation::SystemXcodes)
151            .location(SdkSearchLocation::CommandLineTools)
152            .search::<SimpleSdk>()
153            .unwrap();
154
155        sdks.into_par_iter().for_each(|sdk| {
156            let mut tbd_paths = walkdir::WalkDir::new(sdk.path())
157                .into_iter()
158                .filter_map(|entry| {
159                    let entry = entry.unwrap();
160
161                    let file_name = entry.file_name().to_string_lossy();
162                    if file_name.ends_with(".tbd") {
163                        Some(entry.path().to_path_buf())
164                    } else {
165                        None
166                    }
167                })
168                .collect::<Vec<_>>();
169
170            // We only select a percentage of tbd paths because there are too many
171            // in CI and the test takes too long.
172            let percentage = if let Ok(percentage) = std::env::var("TBD_SAMPLE_PERCENTAGE") {
173                percentage.parse::<usize>().unwrap()
174            } else {
175                10
176            };
177
178            let mut rng = rand::thread_rng();
179            tbd_paths.shuffle(&mut rng);
180
181            for path in tbd_paths.iter().take(tbd_paths.len() * percentage / 100) {
182                eprintln!("parsing {}", path.display());
183                let data = std::fs::read(path).unwrap();
184                let data = String::from_utf8(data).unwrap();
185
186                parse_str(&data).unwrap_or_else(|e| {
187                    eprintln!("path: {}", path.display());
188                    eprint!("{}", data);
189                    eprint!("{:?}", e);
190                    panic!("parse error");
191                });
192            }
193        });
194    }
195}