cargo_meta_proc 0.1.4

Generate Rust data from the Cargo manifest
Documentation
#![feature(proc_macro_diagnostic)]

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro::{ Diagnostic, Level };
use proc_macro2::TokenStream as TokenStream2;
use syn::Result;
use syn::parse::{ Parse, ParseStream };
use quote::quote;
use std::env;
use std::io::prelude::*;
use std::fs::File;
use std::rc::Rc;

struct Args {
    key: syn::LitStr,
}

impl Parse for Args {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Args {
            key: input.parse()?,
        })
    }
}

/// Macro for accessing data from the `package.metadata` section of the Cargo manifest
///
/// # Arguments
/// * `key` - A string slice of a dot-separated path to the TOML key of interest
///
/// # Example
/// Given the following `Cargo.toml`:
/// ```no_run
/// [package]
/// name = "MyApp"
/// version = "0.1.0"
///
/// [package.metadata]
/// copyright = "Copyright (c) 2019 ACME Inc."
/// ```
///
/// And the following `main.rs`:
/// ```no_run
/// #![feature(proc_macro_hygiene)]
///
/// use std::env;
/// use cargo_meta::package_metadata;
///
/// pub fn main() {
///     println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
///     println!("{}", package_metadata!("copyright"));
/// }
/// ```
///
/// Invoking `cargo run` will produce:
/// ```no_run
/// MyApp 0.1.0
/// Copyright (c) 2019 ACME Inc.
/// ```
///
/// ## TOML Support
/// This macro only supports static data:
/// * Integers
/// * Floating-point numbers
/// * Booleans
/// * Keys of tables nested below the `package.metadata` section of the manifest (if the
///   value is a supported type)
/// * Entire TOML arrays
///
/// ## Array Example
/// Given the following Cargo manifest:
/// ```no_run
/// [package.metadata.arrays]
/// some_array = [ 1, 2, 3 ]
/// ```
///
/// This is legal:
/// ```no_run
/// static ARR: [3; i64] = package_metadata!("arrays.some_array");
/// ```
///
/// It does *not* currently support accessing TOML array elements directly, though this
/// is possible.  TOML tables will not be possible without a statically keyed hashmap such
/// as the one provided by the `phf` crate.  Support *may* be added if/when Rust can
/// support static hashmaps with compile-time key errors.
#[proc_macro]
pub fn metadata(tokens: TokenStream) -> TokenStream {
    let args = syn::parse_macro_input!(tokens as Args);
    let manifest = load_manifest();

    let metadata = {
        use toml::value::Table;

        if let Some(package) = manifest.get("package") {
            if let Some(metadata) = package.get("metadata") {
                if let Some(tbl) = metadata.as_table() {
                    tbl.clone()
                } else {
                    let msg = "TOML property has an incorrect type";
                    Diagnostic::new(Level::Error, msg)
                        .note(format!("key `package.metadata`"))
                        .note(format!("expected type `Table`"))
                        .note(format!("actual type `{}`", toml_typename(metadata)))
                        .emit();
                    panic!(msg)
                }
            } else {
                Table::new()
            }
        } else {
            Table::new()
        }
    };

    let key = &args.key.value();
    if let Some(value) = toml_get(&metadata, key) {
        toml_codegen(&value).into()
    } else {
        let msg = "key not present in the Cargo manifest";
        Diagnostic::new(Level::Error, msg)
            .note(format!("key `package.metadata.{}`", key))
            .emit();
        panic!(msg)
    }
}

fn load_manifest() -> toml::value::Value {
    let path = format!("{}/Cargo.toml", env::var("CARGO_MANIFEST_DIR").unwrap());
    let path = Rc::new(path);

    let mut file = {
        let path = Rc::clone(&path);
        File::open(&*path)
            .unwrap_or_else(move |err| {
                let msg = "error occurred opening Cargo manifest";
                Diagnostic::new(Level::Error, msg)
                    .note(format!("{}", err))
                    .note(format!("at `{}`", *path))
                    .emit();
                panic!(msg)
            })
    };

    let mut contents = String::new();
    {
        let path = Rc::clone(&path);
        file.read_to_string(&mut contents)
            .unwrap_or_else(|err| {
                let msg = "error occurred reading Cargo manifest";
                Diagnostic::new(Level::Error, msg)
                    .note(format!("{}", err))
                    .note(format!("at `{}`", *path))
                    .emit();
                panic!(msg)
            });
    }

    toml::from_str(&contents)
        .unwrap_or_else(|err| {
            let msg = "failed to parse Cargo manifest";
            Diagnostic::new(Level::Error, msg)
                .note(format!("{}", err))
                .note(format!("at `{}`", *path))
                .emit();
            panic!(msg)
        })
}

fn toml_get(mut tbl: &toml::value::Table, key: &str) -> Option<toml::value::Value> {
    use toml::value::Value;

    let key_parts: Vec<_> = key.split(".").collect();
    let mut ret = Value::Table(tbl.clone());

    for key in key_parts.iter() {
        match tbl.get(key.clone())? {
            Value::Table(tbl2) => {
                tbl = tbl2;
            },

            value => {
                ret = value.clone();
            },
        }
    }

    Some(ret)
}

fn toml_codegen(value: &toml::value::Value) -> TokenStream2 {
    use toml::value::Value;
    use Value::*;
    match value {
        String(s) => quote! {{
            cargo_meta::id::<&str>(#s)
        }},

        Integer(i) => quote! {{
            cargo_meta::id::<i64>(#i)
        }},

        Float(f) => quote! {{
            cargo_meta::id::<f64>(#f)
        }},

        Boolean(b) => quote! {{
            cargo_meta::id::<bool>(#b)
        }},

        Array(a) => toml_array_codegen(a),

        Table(_t) => {
            let msg = "TOML tables are not supported";
            Diagnostic::new(Level::Error, msg)
                .note(format!("this would require statically keyed hash tables"))
                .emit();
            panic!(msg)
        },

        Datetime(d) => {
            let msg = "TOML dates are emitted as `&'static str`";
            Diagnostic::new(Level::Warning, msg)
                .note(format!("there are no const constructors for `Datetime`"))
                .emit();

            let date_str = toml::ser::to_string(d).unwrap();
            quote! {{
                #date_str
            }}
        },
    }
}

fn toml_typename(value: &toml::value::Value) -> &'static str {
    use toml::value::Value::*;
    match value {
        String(_)   => "String",
        Integer(_)  => "Integer",
        Float(_)    => "Float",
        Boolean(_)  => "Boolean",
        Datetime(_) => "Datetime",
        Array(_)    => "Array",
        Table(_)    => "Table",
    }
}

fn toml_array_codegen(arr: &toml::value::Array) -> TokenStream2 {
    let statements = emit_array_items(arr);
    let emit = quote! {{
        [
            #statements
        ]
    }};

    emit.into()
}

fn emit_array_items(arr: &toml::value::Array) -> TokenStream2 {
    arr.iter().flat_map(|val| {
        let val = toml_codegen(val);
        quote! {
            #val,
        }
    }).collect()
}