Skip to main content

include_cargo_toml2/
lib.rs

1//! This crate provides a macro called [`include_toml!`] which parses properties of `Cargo.toml` at compile time.
2
3extern crate proc_macro;
4extern crate proc_macro2;
5extern crate quote;
6extern crate syn;
7extern crate toml;
8
9use crate::{
10    proc_macro::TokenStream,
11    proc_macro2::{Literal, Span as Span2, TokenStream as TokenStream2},
12    quote::{quote, ToTokens},
13    syn::{
14        parse::{Parse, ParseBuffer},
15        parse_macro_input,
16        token::Dot,
17        Error as SynError, Lit, LitBool,
18    },
19    toml::Value,
20};
21use std::env::var;
22use std::fs::read_to_string;
23use std::path::{Path, PathBuf};
24
25/// Helper that stores either integer or string.
26///
27/// Used to create vector of indexing items in [`TomlIndex`].
28enum Index {
29    Int(usize),
30    Str(String),
31}
32
33/// Struct that parses input of [`include_toml`].
34///
35/// Input should consist of either string literals or integers separated by dots.
36struct TomlIndex(Vec<Index>);
37
38impl Parse for TomlIndex {
39    fn parse(input: &ParseBuffer) -> Result<Self, SynError> {
40        let mut another_one = true;
41        let mut index = Vec::new();
42        while another_one {
43            index.push(match input.parse::<Lit>() {
44                Ok(lit) => match lit {
45                    Lit::Str(lit_str) => Index::Str(lit_str.value()),
46                    Lit::Int(lit_int) => Index::Int(
47                        lit_int
48                            .base10_digits()
49                            .parse()
50                            .expect("Cannot parse literal integer"),
51                    ),
52                    _ => return Err(SynError::new(input.span(), "Unsupported literal")),
53                },
54                Err(e) => {
55                    return Err(SynError::new(
56                        input.span(),
57                        format!("Cannot parse index item: {}", e),
58                    ))
59                }
60            });
61            if let Err(_) = input.parse::<Dot>() {
62                another_one = false;
63            }
64        }
65        Ok(Self(index))
66    }
67}
68
69/// Converts any TOML value to valid Rust types.
70fn translate(input: Value) -> TokenStream2 {
71    match input {
72        Value::String(s) => Lit::new(Literal::string(&s)).to_token_stream().into(),
73        Value::Integer(i) => Lit::new(Literal::i64_suffixed(i)).to_token_stream().into(),
74        Value::Float(f) => Lit::new(Literal::f64_suffixed(f)).to_token_stream().into(),
75        Value::Datetime(d) => Lit::new(Literal::string(&d.to_string()))
76            .to_token_stream()
77            .into(),
78        Value::Boolean(b) => Lit::Bool(LitBool::new(b, Span2::call_site()))
79            .to_token_stream()
80            .into(),
81        Value::Array(a) => {
82            let mut ts = TokenStream2::new();
83            for value in a {
84                let v = translate(value);
85                ts.extend(quote! (#v,));
86            }
87            quote! ((#ts))
88        }
89        Value::Table(t) => {
90            let mut ts = TokenStream2::new();
91            for (key, value) in t {
92                let v = translate(value);
93                ts.extend(quote! ((#key, #v)));
94            }
95            quote! ((#ts))
96        }
97    }
98}
99
100/// Parse `Cargo.toml` at compile time.
101///
102/// # TOML to Rust conversion
103///
104/// - TOML [string](Value::String) -> Rust [`&str`]
105/// - TOML [integer](Value::Integer) -> Rust [`i64`]
106/// - TOML [float](Value::Float) -> Rust [`f64`]
107/// - TOML [boolean](Value::Boolean) -> Rust [`bool`]
108/// - TOML [datetime](Value::Datetime) -> Rust [`&str`]
109/// - TOML [array](Value::Array) -> Rust tuple \
110///     TOML arrays can hold different types, Rust [`Vec`]s can't.
111/// - TOML [table](Value::Table) -> Rust tuple \
112///     TOML tables can hold different types, Rust [`Vec`]s can't.
113///
114/// # Example
115///
116/// Keys to index `Cargo.toml` are parsed as string literals and array / table indexes are parsed as integer literals:
117///
118/// ```rust
119/// use include_cargo_toml2::include_toml;
120///
121/// assert_eq!(
122///     include_toml!("package"."version"),
123///     "0.3.1"
124/// );
125/// assert_eq!(
126///     include_toml!("package"."name"),
127///     "include-cargo-toml2"
128/// );
129/// // indexing array with literal 2
130/// assert_eq!(
131///     include_toml!("package"."keywords".2),
132///     "Cargo-toml"
133/// );
134/// assert_eq!(
135///     include_toml!("lib"."proc-macro"),
136///     true
137/// );
138/// ```
139///
140/// Because TOML's arrays and tables do not work like [`Vec`] and [`HashMap`](std::collections::HashMap), tuples are used.
141///
142/// ```rust
143/// use include_cargo_toml2::include_toml;
144///
145/// assert_eq!(
146///     include_toml!("package"."keywords"),
147///     ("macro", "version", "Cargo-toml", "compile-time", "parse")
148/// );
149/// ```
150///
151/// Leading or trailing dots are not allowed:
152///
153/// ```rust,compile_fail
154/// use include_cargo_toml::include_toml;
155///
156/// let this_fails = include_toml!(."package"."name");
157/// let this_fails_too = include_toml!("package"."name".);
158/// ```
159#[proc_macro]
160pub fn include_toml(input: TokenStream) -> TokenStream {
161    let dir = var("CARGO_MANIFEST_DIR").expect("Environment variable CARGO_MANIFEST_DIR not set!");
162    let path = Path::new(&dir).join("Cargo.toml");
163
164    let cargo_toml = parse(&path);
165    let index: TomlIndex = parse_macro_input!(input);
166    let result = lookup(index, cargo_toml);
167
168    translate(result).into()
169}
170
171fn parse(path: &PathBuf) -> Value {
172    let content = read_to_string(path).expect("Cannot read Cargo.toml");
173    content.parse::<Value>().expect("Cannot parse Cargo.toml to json")
174}
175
176fn lookup(index: TomlIndex, mut toml: Value) -> Value {
177    for item in index.0 {
178        match item {
179            Index::Int(index) => {
180                toml = toml[index].clone();
181            }
182            Index::Str(index) => {
183                toml = toml[index].clone();
184            }
185        }
186    }
187    toml
188}
189
190#[cfg(test)]
191mod tests {
192    use crate::{lookup, parse};
193    use std::env::var;
194    use std::path::Path;
195    use toml::Value;
196
197    #[test]
198    fn should_parse_when_cargo_toml_is_valid() {
199        let dir = var("CARGO_MANIFEST_DIR").expect("Environment variable CARGO_MANIFEST_DIR must be set!");
200
201        let path = Path::new(&dir).join("Cargo.toml");
202        println!("{}", dir);
203        let toml = parse(&path);
204
205        assert_eq!("include-cargo-toml2", toml["package"]["name"].as_str().unwrap());
206    }
207
208    #[test]
209    fn should_fetch_attribute_when_cargo_toml_is_given() {
210        let cargo_toml = r#"
211        [package]
212        edition="2021"
213        version="0.1.0"
214        "#;
215
216        let toml: Value = cargo_toml.parse::<Value>().expect("Cannot parse Cargo.toml");
217        let index = syn::parse_str(r#""package"."version""#).unwrap();
218
219        let result = lookup(index, toml);
220
221        assert_eq!("0.1.0", result.as_str().unwrap());
222    }
223
224    #[test]
225    fn should_fetch_custom_attribute_when_cargo_toml_is_given() {
226        let cargo_toml = r#"
227        [package]
228        edition="2021"
229        [package.metadata.deb]
230        revision=4
231        "#;
232
233        let toml: Value = cargo_toml.parse::<Value>().expect("Cannot parse Cargo.toml");
234        let index = syn::parse_str(r#""package"."metadata"."deb"."revision""#).unwrap();
235
236        let result = lookup(index, toml);
237
238        assert_eq!(4, result.as_integer().unwrap());
239    }
240}