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}