include_cargo_toml/
lib.rs

1//! This crate provides a macro called [`include_toml!`] which parses properties of `Cargo.toml` at compile time.
2
3extern crate cargo_toml;
4extern crate proc_macro;
5extern crate proc_macro2;
6extern crate quote;
7extern crate syn;
8extern crate toml;
9
10use crate::{
11    cargo_toml::Manifest,
12    proc_macro::TokenStream,
13    proc_macro2::{Literal, Span as Span2, TokenStream as TokenStream2},
14    quote::{quote, ToTokens},
15    syn::{
16        parse::{Parse, ParseBuffer},
17        parse_macro_input,
18        token::Dot,
19        Error as SynError, Lit, LitBool,
20    },
21    toml::Value,
22};
23
24/// Helper that stores either integer or string.
25///
26/// Used to create vector of indexing items in [`TomlIndex`].
27enum Index {
28    Int(usize),
29    Str(String),
30}
31
32/// Struct that parses input of [`include_toml`].
33///
34/// Input should consist of either string literals or integers separated by dots.
35struct TomlIndex(Vec<Index>);
36
37impl Parse for TomlIndex {
38    fn parse(input: &ParseBuffer) -> Result<Self, SynError> {
39        let mut another_one = true;
40        let mut index = Vec::new();
41        while another_one {
42            index.push(match input.parse::<Lit>() {
43                Ok(lit) => match lit {
44                    Lit::Str(lit_str) => Index::Str(lit_str.value()),
45                    Lit::Int(lit_int) => Index::Int(
46                        lit_int
47                            .base10_digits()
48                            .parse()
49                            .expect("Cannot parse literal integer"),
50                    ),
51                    _ => return Err(SynError::new(input.span(), "Unsupported literal")),
52                },
53                Err(e) => {
54                    return Err(SynError::new(
55                        input.span(),
56                        format!("Cannot parse index item: {}", e),
57                    ))
58                }
59            });
60            if let Err(_) = input.parse::<Dot>() {
61                another_one = false;
62            }
63        }
64        Ok(Self(index))
65    }
66}
67
68/// Converts any TOML value to valid Rust types.
69fn toml_to_ts(input: Value) -> TokenStream2 {
70    match input {
71        Value::String(s) => Lit::new(Literal::string(&s)).to_token_stream().into(),
72        Value::Integer(i) => Lit::new(Literal::i64_suffixed(i)).to_token_stream().into(),
73        Value::Float(f) => Lit::new(Literal::f64_suffixed(f)).to_token_stream().into(),
74        Value::Datetime(d) => Lit::new(Literal::string(&d.to_string()))
75            .to_token_stream()
76            .into(),
77        Value::Boolean(b) => Lit::Bool(LitBool::new(b, Span2::call_site()))
78            .to_token_stream()
79            .into(),
80        Value::Array(a) => {
81            let mut ts = TokenStream2::new();
82            for value in a {
83                let v = toml_to_ts(value);
84                ts.extend(quote! (#v,));
85            }
86            quote! ((#ts))
87        }
88        Value::Table(t) => {
89            let mut ts = TokenStream2::new();
90            for (key, value) in t {
91                let v = toml_to_ts(value);
92                ts.extend(quote! ((#key, #v)));
93            }
94            quote! ((#ts))
95        }
96    }
97}
98
99/// Parse `Cargo.toml` at compile time.
100///
101/// # TOML to Rust conversion
102///
103/// - TOML [string](Value::String) -> Rust [`&str`]
104/// - TOML [integer](Value::Integer) -> Rust [`i64`]
105/// - TOML [float](Value::Float) -> Rust [`f64`]
106/// - TOML [boolean](Value::Boolean) -> Rust [`bool`]
107/// - TOML [datetime](Value::Datetime) -> Rust [`&str`]
108/// - TOML [array](Value::Array) -> Rust tuple \
109///     TOML arrays can hold different types, Rust [`Vec`]s can't.
110/// - TOML [table](Value::Table) -> Rust tuple \
111///     TOML tables can hold different types, Rust [`Vec`]s can't.
112///
113/// # Example
114///
115/// Keys to index `Cargo.toml` are parsed as string literals and array / table indexes are parsed as integer literals:
116///
117/// ```rust
118/// use include_cargo_toml::include_toml;
119///
120/// assert_eq!(
121///     include_toml!("package"."version"),
122///     "0.1.0"
123/// );
124/// assert_eq!(
125///     include_toml!("package"."name"),
126///     "include-cargo-toml"
127/// );
128/// // indexing array with literal 2
129/// assert_eq!(
130///     include_toml!("package"."keywords".2),
131///     "Cargo-toml"
132/// );
133/// assert_eq!(
134///     include_toml!("lib"."proc-macro"),
135///     true
136/// );
137/// ```
138///
139/// Because TOML's arrays and tables do not work like [`Vec`] and [`HashMap`](std::collections::HashMap), tuples are used.
140///
141/// ```rust
142/// use include_cargo_toml::include_toml;
143///
144/// assert_eq!(
145///     include_toml!("package"."keywords"),
146///     ("macro", "version", "Cargo-toml", "compile-time", "parse")
147/// );
148/// ```
149///
150/// Leading or trailing dots are not allowed:
151///
152/// ```rust,compile_fail
153/// use include_cargo_toml::include_toml;
154///
155/// let this_fails = include_toml!(."package"."name");
156/// let this_fails_too = include_toml!("package"."name".);
157/// ```
158#[proc_macro]
159pub fn include_toml(input: TokenStream) -> TokenStream {
160    // parse input
161    let input: TomlIndex = parse_macro_input!(input);
162    // get Cargo.toml contents
163    // using Manifest here eliminates subfolder problems
164    let cargo_toml: Manifest =
165        Manifest::from_path_with_metadata("Cargo.toml").expect("Cannot read Cargo.toml");
166    // parse Cargo.toml contents as TOML
167    let mut cargo_toml_toml: Value =
168        Value::try_from(cargo_toml).expect("Cannot parse Cargo.toml to json");
169    // get wanted field by traversing through TOML structure
170    for item in input.0 {
171        match item {
172            Index::Int(index) => {
173                cargo_toml_toml = cargo_toml_toml[index].clone();
174            }
175            Index::Str(index) => {
176                cargo_toml_toml = cargo_toml_toml[index].clone();
177            }
178        }
179    }
180    // convert toml value to TokenStream
181    toml_to_ts(cargo_toml_toml).into()
182}