// Copyright 2022 Oxide Computer Company //! Macros for the progenitor OpenAPI client generator. #![deny(missing_docs)] use std::{ collections::HashMap, fmt::Display, fs::File, path::{Path, PathBuf}, }; use openapiv3::OpenAPI; use proc_macro::TokenStream; use progenitor_impl::{ CrateVers, GenerationSettings, Generator, InterfaceStyle, TagStyle, TypePatch, UnknownPolicy, }; use quote::{quote, ToTokens}; use schemars::schema::SchemaObject; use serde::Deserialize; use serde_tokenstream::{OrderedMap, ParseWrapper}; use syn::LitStr; use token_utils::TypeAndImpls; mod token_utils; /// Generates a client from the given OpenAPI document /// /// `generate_api!` can be invoked in two ways. The simple form, takes a path /// to the OpenAPI document: /// ```ignore /// generate_api!("path/to/spec.json"); /// ``` /// /// The more complex form accepts the following key-value pairs in any order: /// ```ignore /// generate_api!( /// spec = "path/to/spec.json", /// [ interface = ( Positional | Builder ), ] /// [ tags = ( Merged | Separate ), ] /// [ pre_hook = closure::or::path::to::function, ] /// [ post_hook = closure::or::path::to::function, ] /// /// [ derives = [ path::to::DeriveMacro ], ] /// /// [ unknown_crates = (Generate | Allow | Deny ), ] /// [ crates = { "" = ("" | "*" | "!" ) } ] /// /// [ patch = { TypeName = { [rename = NewTypeName], [derives = []] }, } ] /// [ replace = { TypeName = full_path::to::other::TypeName, }] /// [ convert = { { } = full_path::to::TypeName, }] /// /// ); /// ``` /// /// The `spec` key is required; it is the OpenAPI document (JSON or YAML) from /// which the client is derived. /// /// The optional `interface` lets you specify either a `Positional` argument or /// `Builder` argument style; `Positional` is the default. /// /// The optional `tags` may be `Merged` in which case all operations are /// methods on the `Client` struct or `Separate` in which case each tag is /// represented by an "extension trait" that `Client` implements. The default /// is `Merged`. /// /// The optional `inner_type` is for ancillary data, stored with the generated /// client that can be usd by the pre and post hooks. /// /// The optional `pre_hook` is either a closure (that must be within /// parentheses: `(fn |inner, request| { .. })`) or a path to a function. The /// closure or function must take two parameters: the inner type and a /// `&reqwest::Request`. This allows clients to examine requests before they're /// sent to the server, for example to log them. /// /// The optional `post_hook` is either a closure (that must be within /// parentheses: `(fn |inner, result| { .. })`) or a path to a function. The /// closure or function must take two parameters: the inner type and a /// `&Result`. This allows clients to /// examine responses, for example to log them. /// /// Additional options control type generation: /// - `derives`: optional array of derive macro paths; the derive macros to be /// applied to all generated types /// /// - `struct_builder`: optional boolean; (if true) generates a `::builder()` /// method for each generated struct that can be used to specify each /// property and construct the struct /// /// - `unknown_crates`: optional policy regarding the handling of schemas that /// contain the `x-rust-type` extension whose crates are not explicitly named /// in the `crates` section. The options are `generate` to ignore the /// extension and generate a *de novo* type, `allow` to use the named type /// (which may require the addition of a new dependency to compile, and which /// ignores version compatibility checks), or `deny` to produce a /// compile-time error (requiring the user to specify the crate's disposition /// in the `crates` section). /// /// - `crates`: optional map from crate name to the version of the crate in /// use. Types encountered with the Rust type extension (`x-rust-type`) will /// use types from the specified crates rather than generating them (within /// the constraints of type compatibility). /// /// - `patch`: optional map from type to an object with the optional members /// `rename` and `derives`. This may be used to renamed generated types or /// to apply additional (non-default) derive macros to them. /// /// - `replace`: optional map from definition name to a replacement type. This /// may be used to skip generation of the named type and use a existing Rust /// type. /// /// - `convert`: optional map from a JSON schema type defined in `$defs` to a /// replacement type. This may be used to skip generation of the schema and /// use an existing Rust type. #[proc_macro] pub fn generate_api(item: TokenStream) -> TokenStream { match do_generate_api(item) { Err(err) => err.to_compile_error().into(), Ok(out) => out, } } #[derive(Deserialize)] struct MacroSettings { spec: ParseWrapper, #[serde(default)] interface: InterfaceStyle, #[serde(default)] tags: TagStyle, inner_type: Option>, pre_hook: Option>, pre_hook_async: Option>, post_hook: Option>, #[serde(default)] derives: Vec>, #[serde(default)] unknown_crates: UnknownPolicy, #[serde(default)] crates: HashMap, #[serde(default)] patch: HashMap, MacroPatch>, #[serde(default)] replace: HashMap, ParseWrapper>, #[serde(default)] convert: OrderedMap>, } #[derive(Deserialize)] enum MacroSettingsImpl { Display, } impl Display for MacroSettingsImpl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MacroSettingsImpl::Display => f.write_str("Display"), } } } #[derive(Deserialize)] struct MacroPatch { #[serde(default)] rename: Option, #[serde(default)] derives: Vec>, } impl From for TypePatch { fn from(a: MacroPatch) -> Self { let mut s = Self::default(); a.rename.iter().for_each(|rename| { s.with_rename(rename); }); a.derives.iter().for_each(|derive| { s.with_derive(derive.to_token_stream().to_string()); }); s } } #[derive(Deserialize)] enum GenerationStyle { Positional, Builder, } impl Default for GenerationStyle { fn default() -> Self { Self::Positional } } #[derive(Debug)] struct ClosureOrPath(proc_macro2::TokenStream); impl syn::parse::Parse for ClosureOrPath { fn parse(input: syn::parse::ParseStream) -> syn::Result { let lookahead = input.lookahead1(); if lookahead.peek(syn::token::Paren) { let group: proc_macro2::Group = input.parse()?; return syn::parse2::(group.stream()); } if let Ok(closure) = input.parse::() { return Ok(Self(closure.to_token_stream())); } input .parse::() .map(|path| Self(path.to_token_stream())) } } struct MacroCrateSpec { original: Option, version: CrateVers, } impl<'de> Deserialize<'de> for MacroCrateSpec { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let ss = String::deserialize(deserializer)?; let (original, vers_str) = if let Some(ii) = ss.find('@') { let original_str = &ss[..ii]; let rest = &ss[ii + 1..]; if !is_crate(original_str) { return Err(::invalid_value( serde::de::Unexpected::Str(&ss), &"valid crate name", )); } (Some(original_str.to_string()), rest) } else { (None, ss.as_ref()) }; let Some(version) = CrateVers::parse(vers_str) else { return Err(::invalid_value( serde::de::Unexpected::Str(&ss), &"valid version", )); }; Ok(Self { original, version }) } } #[derive(Hash, PartialEq, Eq)] struct CrateName(String); impl<'de> Deserialize<'de> for CrateName { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let ss = String::deserialize(deserializer)?; if is_crate(&ss) { Ok(Self(ss)) } else { Err(::invalid_value( serde::de::Unexpected::Str(&ss), &"valid crate name", )) } } } fn is_crate(s: &str) -> bool { !s.contains(|cc: char| !cc.is_alphanumeric() && cc != '_' && cc != '-') } fn open_file( path: PathBuf, span: proc_macro2::Span, ) -> Result { File::open(path.clone()).map_err(|e| { let path_str = path.to_string_lossy(); syn::Error::new(span, format!("couldn't read file {}: {}", path_str, e)) }) } fn do_generate_api(item: TokenStream) -> Result { let (spec, settings) = if let Ok(spec) = syn::parse::(item.clone()) { (spec, GenerationSettings::default()) } else { let MacroSettings { spec, interface, tags, inner_type, pre_hook, pre_hook_async, post_hook, unknown_crates, crates, derives, patch, replace, convert, } = serde_tokenstream::from_tokenstream(&item.into())?; let mut settings = GenerationSettings::default(); settings.with_interface(interface); settings.with_tag(tags); inner_type.map(|inner_type| { settings.with_inner_type(inner_type.to_token_stream()) }); pre_hook .map(|pre_hook| settings.with_pre_hook(pre_hook.into_inner().0)); pre_hook_async.map(|pre_hook_async| { settings.with_pre_hook_async(pre_hook_async.into_inner().0) }); post_hook .map(|post_hook| settings.with_post_hook(post_hook.into_inner().0)); settings.with_unknown_crates(unknown_crates); crates.into_iter().for_each( |(CrateName(crate_name), MacroCrateSpec { original, version })| { if let Some(original_crate) = original { settings.with_crate( original_crate, version, Some(&crate_name), ); } else { settings.with_crate(crate_name, version, None); } }, ); derives.into_iter().for_each(|derive| { settings.with_derive(derive.to_token_stream()); }); patch.into_iter().for_each(|(type_name, patch)| { settings.with_patch( type_name.to_token_stream().to_string(), &patch.into(), ); }); replace.into_iter().for_each(|(type_name, type_and_impls)| { let type_name = type_name.to_token_stream(); let (replace_name, impls) = type_and_impls.into_inner().into_name_and_impls(); settings.with_replacement(type_name, replace_name, impls); }); convert.into_iter().for_each(|(schema, type_and_impls)| { let (type_name, impls) = type_and_impls.into_inner().into_name_and_impls(); settings.with_conversion(schema, type_name, impls); }); (spec.into_inner(), settings) }; let dir = std::env::var("CARGO_MANIFEST_DIR").map_or_else( |_| std::env::current_dir().unwrap(), |s| Path::new(&s).to_path_buf(), ); let path = dir.join(spec.value()); let path_str = path.to_string_lossy(); let mut f = open_file(path.clone(), spec.span())?; let oapi: OpenAPI = match serde_json::from_reader(f) { Ok(json_value) => json_value, _ => { f = open_file(path.clone(), spec.span())?; serde_yaml::from_reader(f).map_err(|e| { syn::Error::new( spec.span(), format!("failed to parse {}: {}", path_str, e), ) })? } }; let mut builder = Generator::new(&settings); let code = builder.generate_tokens(&oapi).map_err(|e| { syn::Error::new( spec.span(), format!("generation error for {}: {}", spec.value(), e), ) })?; let output = quote! { // The progenitor_client is tautologically visible from macro // consumers. use progenitor::progenitor_client; #code // Force a rebuild when the given file is modified. const _: &str = include_str!(#path_str); }; Ok(output.into()) }