octorest_build/
lib.rs

1use std::{env, fs, io, path::Path, path::PathBuf};
2
3use cfg_if::cfg_if;
4use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree};
5
6macro_rules! err {
7    ($format:literal $(, $($tt:tt)*)?) => {{
8        |err| ::std::io::Error::new(::std::io::ErrorKind::Other,
9            format!(concat!($format, ": {}"), $($($tt)*, )? err))
10    }}
11}
12
13mod gen;
14mod idents;
15mod schema;
16
17pub fn main() -> Result<(), Box<dyn std::error::Error>> {
18    #[cfg(feature = "dev")]
19    {
20        pretty_env_logger::init();
21    }
22
23    #[derive(serde::Deserialize)]
24    struct ReleaseData {
25        tag_name: String,
26        assets: Vec<Asset>,
27    }
28    #[derive(serde::Deserialize)]
29    struct Asset {
30        name: String,
31        browser_download_url: String,
32    }
33
34    let out_dir = PathBuf::from(&env::var("OUT_DIR").expect("defined by cargo"));
35
36    let pkg_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("api.github.com.json");
37    let json_path = if pkg_path.exists() {
38        pkg_path
39    } else {
40        cfg_if! {
41            if #[cfg(feature = "online")] {
42                let client = reqwest::blocking::Client::new();
43                let data = client.get("https://api.github.com/repos/octokit/routes/releases/latest")
44                    .send().map_err(err!("Error fetching latest routes"))?
45                    .error_for_status().map_err(err!("Error fetching latest routes"))?
46                    .json::<ReleaseData>().map_err(err!("Error parsing routes release data"))?;
47
48                let json_path = out_dir.join(&format!("{}.json", &data.tag_name));
49                if !json_path.exists() {
50                    log::info!("Downloading new routes version {}", &data.tag_name);
51                    let url = match data.assets.iter().filter(|asset| &asset.name == "api.github.com.json")
52                        .map(|asset| &asset.browser_download_url)
53                        .next() {
54                            Some(url) => url,
55                            None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Latest octokit/routes release does not contain api.github.com.json"))),
56                        };
57                    let mut file = fs::File::create(url).map_err(Box::new)?;
58                    let _ = client.get(url) // we don't care about number of bytes written
59                        .send().map_err(Box::new)?
60                        .error_for_status().map_err(Box::new)?
61                        .copy_to(&mut file).map_err(Box::new)?;
62                }
63                json_path
64            } else {
65                return Err(Box::new(io::Error::new(io::ErrorKind::Other, "`online` feature not enabled, but no packaged api.github.com.json is available")));
66            }
67        }
68    };
69
70    let out = out_dir.join("out.rs");
71    let types = out_dir.join("types.rs");
72    let mut out = fs::File::create(out)?;
73    let mut types = fs::File::create(types)?;
74
75    let (ts_out, ts_types) = run(&json_path)?;
76
77    write_token_stream(ts_out, &mut out, 0, &mut true)?;
78    write_token_stream(ts_types, &mut types, 0, &mut true)?;
79    Ok(())
80}
81
82fn run(json_path: &Path) -> io::Result<(TokenStream, TokenStream)> {
83    let index = schema::parse(json_path)?;
84    let (apis, types) = gen::gen(index);
85
86    Ok((apis, types))
87}
88
89fn write_token_stream(
90    ts: TokenStream,
91    f: &mut fs::File,
92    indent: usize,
93    start_of_line: &mut bool,
94) -> io::Result<()> {
95    use std::io::Write;
96
97    for token in ts {
98        if *start_of_line {
99            write!(f, "{}", "    ".repeat(indent))?;
100            *start_of_line = false;
101        }
102        match token {
103            TokenTree::Literal(literal) => write!(f, "{}", literal)?,
104            TokenTree::Ident(ident) => write!(f, "{} ", ident)?,
105            TokenTree::Punct(punct) => {
106                match punct.spacing() {
107                    Spacing::Alone => write!(f, "{} ", punct.as_char())?,
108                    Spacing::Joint => write!(f, "{}", punct.as_char())?,
109                }
110                if punct.as_char() == ';' {
111                    writeln!(f)?;
112                    *start_of_line = true;
113                }
114            }
115            TokenTree::Group(group) => match group.delimiter() {
116                Delimiter::Parenthesis => {
117                    write!(f, "(")?;
118                    write_token_stream(group.stream(), f, indent + 2, start_of_line)?;
119                    write!(f, ")")?;
120                }
121                Delimiter::Bracket => {
122                    write!(f, "[")?;
123                    write_token_stream(group.stream(), f, indent + 2, start_of_line)?;
124                    write!(f, "]")?;
125                }
126                Delimiter::Brace => {
127                    writeln!(f, "{{")?;
128                    *start_of_line = true;
129                    write_token_stream(group.stream(), f, indent + 1, start_of_line)?;
130                    writeln!(f)?;
131                    write!(
132                        f,
133                        "{}}}",
134                        if *start_of_line {
135                            "    ".repeat(indent)
136                        } else {
137                            String::new()
138                        }
139                    )?;
140                    if *start_of_line {
141                        writeln!(f)?;
142                    }
143                }
144                Delimiter::None => write_token_stream(group.stream(), f, indent, start_of_line)?,
145            },
146        }
147    }
148    Ok(())
149}