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