From 909e80841f7a4d2a740659d03ee1ef94f42b0078 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 12 Jul 2022 22:42:34 +0200 Subject: [PATCH] macros: Ensure that crates using ruma_api macro have client and server features --- crates/ruma-macros/Cargo.toml | 3 ++ crates/ruma-macros/src/api.rs | 59 ++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/ruma-macros/Cargo.toml b/crates/ruma-macros/Cargo.toml index 86b92710..abeff098 100644 --- a/crates/ruma-macros/Cargo.toml +++ b/crates/ruma-macros/Cargo.toml @@ -18,8 +18,11 @@ proc-macro = true compat = [] [dependencies] +once_cell = "1.13.0" proc-macro-crate = "1.0.0" proc-macro2 = "1.0.24" quote = "1.0.8" ruma-identifiers-validation = { version = "0.8.1", path = "../ruma-identifiers-validation", default-features = false } +serde = { version = "1.0.139", features = ["derive"] } syn = { version = "1.0.57", features = ["extra-traits", "full", "visit"] } +toml = "0.5.9" diff --git a/crates/ruma-macros/src/api.rs b/crates/ruma-macros/src/api.rs index 70250d62..41dfb797 100644 --- a/crates/ruma-macros/src/api.rs +++ b/crates/ruma-macros/src/api.rs @@ -1,7 +1,11 @@ //! Methods and types for generating API endpoints. -use proc_macro2::TokenStream; +use std::{env, fs, path::Path}; + +use once_cell::sync::Lazy; +use proc_macro2::{Span, TokenStream}; use quote::quote; +use serde::{de::IgnoredAny, Deserialize}; use syn::{ braced, parse::{Parse, ParseStream}, @@ -46,6 +50,8 @@ pub struct Api { impl Api { pub fn expand_all(self) -> TokenStream { + let maybe_error = ensure_feature_presence().map(syn::Error::to_compile_error); + let ruma_common = import_ruma_common(); let http = quote! { #ruma_common::exports::http }; @@ -73,6 +79,8 @@ impl Api { let metadata_doc = format!("Metadata for the `{}` API endpoint.", name.value()); quote! { + #maybe_error + #[doc = #metadata_doc] pub const METADATA: #ruma_common::api::Metadata = #ruma_common::api::Metadata { description: #description, @@ -158,3 +166,52 @@ fn parse_response(input: ParseStream<'_>, attributes: Vec) -> syn::Re Ok(Response { attributes, fields, response_kw }) } + +// Returns an error with a helpful error if the crate `ruma_api!` is used from doesn't declare both +// a `client` and a `server` feature. +fn ensure_feature_presence() -> Option<&'static syn::Error> { + #[derive(Deserialize)] + struct CargoToml { + features: Features, + } + + #[derive(Deserialize)] + struct Features { + client: Option, + server: Option, + } + + static RESULT: Lazy> = Lazy::new(|| { + let manifest_dir = env::var("CARGO_MANIFEST_DIR") + .map_err(|_| syn::Error::new(Span::call_site(), "Failed to read CARGO_MANIFEST_DIR"))?; + + let manifest_file = Path::new(&manifest_dir).join("Cargo.toml"); + let manifest_bytes = fs::read(manifest_file) + .map_err(|_| syn::Error::new(Span::call_site(), "Failed to read Cargo.toml"))?; + + let manifest_parsed: CargoToml = toml::from_slice(&manifest_bytes) + .map_err(|_| syn::Error::new(Span::call_site(), "Failed to parse Cargo.toml"))?; + + if manifest_parsed.features.client.is_none() { + return Err(syn::Error::new( + Span::call_site(), + "This crate doesn't define a `client` feature in its `Cargo.toml`.\n\ + Please add a `client` feature such that generated `OutgoingRequest` and \ + `IncomingResponse` implementations can be enabled.", + )); + } + + if manifest_parsed.features.server.is_none() { + return Err(syn::Error::new( + Span::call_site(), + "This crate doesn't define a `server` feature in its `Cargo.toml`.\n\ + Please add a `server` feature such that generated `IncomingRequest` and \ + `OutgoingResponse` implementations can be enabled.", + )); + } + + Ok(()) + }); + + RESULT.as_ref().err() +}