#![warn(clippy::all, missing_docs)]
#![allow(clippy::needless_doctest_main)]
#![recursion_limit = "256"]
use crate::context::Context;
use crate::types::{ConjureDefinition, TypeDefinition};
use failure::{bail, Error, ResultExt};
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
mod aliases;
mod cargo_toml;
mod clients;
mod context;
mod enums;
mod errors;
mod http_paths;
mod objects;
mod servers;
#[allow(dead_code, clippy::all)]
#[rustfmt::skip]
mod types;
mod unions;
#[cfg(feature = "example-types")]
#[allow(warnings)]
#[rustfmt::skip]
pub mod example_types;
struct CrateInfo {
name: String,
version: String,
}
pub struct Config {
exhaustive: bool,
staged_builders: bool,
strip_prefix: Option<String>,
version: Option<String>,
build_crate: Option<CrateInfo>,
}
impl Default for Config {
fn default() -> Config {
Config::new()
}
}
impl Config {
pub fn new() -> Config {
Config {
exhaustive: false,
staged_builders: false,
strip_prefix: None,
version: None,
build_crate: None,
}
}
pub fn exhaustive(&mut self, exhaustive: bool) -> &mut Config {
self.exhaustive = exhaustive;
self
}
pub fn staged_builders(&mut self, staged_builders: bool) -> &mut Config {
self.staged_builders = staged_builders;
self
}
#[deprecated(note = "no longer used", since = "1.2.0")]
pub fn run_rustfmt(&mut self, _run_rustfmt: bool) -> &mut Config {
self
}
#[deprecated(note = "no longer used", since = "1.2.0")]
pub fn rustfmt<T>(&mut self, _rustfmt: T) -> &mut Config
where
T: AsRef<OsStr>,
{
self
}
pub fn strip_prefix<T>(&mut self, strip_prefix: T) -> &mut Config
where
T: Into<Option<String>>,
{
self.strip_prefix = strip_prefix.into();
self
}
pub fn version<T>(&mut self, version: T) -> &mut Config
where
T: Into<Option<String>>,
{
self.version = version.into();
self
}
pub fn build_crate(&mut self, name: &str, version: &str) -> &mut Config {
self.build_crate = Some(CrateInfo {
name: name.to_string(),
version: version.to_string(),
});
self
}
pub fn generate_files<P, Q>(&self, ir_file: P, out_dir: Q) -> Result<(), Error>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
self.generate_files_inner(ir_file.as_ref(), out_dir.as_ref())
}
fn generate_files_inner(&self, ir_file: &Path, out_dir: &Path) -> Result<(), Error> {
let defs = self.parse_ir(ir_file)?;
if defs.version() != 1 {
bail!("unsupported IR version {}", defs.version());
}
let modules = self.create_modules(&defs);
let (src_dir, lib_root) = if self.build_crate.is_some() {
(out_dir.join("src"), true)
} else {
(out_dir.to_path_buf(), false)
};
if let Some(info) = &self.build_crate {
self.write_cargo_toml(out_dir, info, &defs)?;
self.write_rustfmt_toml(out_dir)?;
}
modules.render(&src_dir, lib_root)?;
Ok(())
}
fn parse_ir(&self, ir_file: &Path) -> Result<ConjureDefinition, Error> {
let ir = fs::read_to_string(ir_file)
.with_context(|_| format!("error reading file {}", ir_file.display()))?;
let defs = conjure_serde::json::client_from_str(&ir)
.with_context(|_| format!("error parsing Conjure IR file {}", ir_file.display()))?;
Ok(defs)
}
fn create_modules(&self, defs: &ConjureDefinition) -> ModuleTrie {
let context = Context::new(
defs,
self.exhaustive,
self.staged_builders,
self.strip_prefix.as_deref(),
self.version
.as_deref()
.or_else(|| self.build_crate.as_ref().map(|v| &*v.version)),
);
let mut root = ModuleTrie::new();
for def in defs.types() {
let (type_name, contents) = match def {
TypeDefinition::Enum(def) => (def.type_name(), enums::generate(&context, def)),
TypeDefinition::Alias(def) => (def.type_name(), aliases::generate(&context, def)),
TypeDefinition::Union(def) => (def.type_name(), unions::generate(&context, def)),
TypeDefinition::Object(def) => (def.type_name(), objects::generate(&context, def)),
};
let type_ = Type {
module_name: context.module_name(type_name),
type_names: vec![context.type_name(type_name.name()).to_string()],
contents,
};
root.insert(&context.module_path(type_name), type_);
}
for def in defs.errors() {
let type_ = Type {
module_name: context.module_name(def.error_name()),
type_names: vec![context.type_name(def.error_name().name()).to_string()],
contents: errors::generate(&context, def),
};
root.insert(&context.module_path(def.error_name()), type_);
}
for def in defs.services() {
let client = clients::generate(&context, def);
let server = servers::generate(&context, def);
let contents = quote! {
#client
#server
};
let type_ = Type {
module_name: context.module_name(def.service_name()),
type_names: vec![
format!("{}Client", def.service_name().name()),
format!("{}AsyncClient", def.service_name().name()),
context.type_name(def.service_name().name()).to_string(),
format!("Async{}", def.service_name().name()),
format!("{}Endpoints", def.service_name().name()),
],
contents,
};
root.insert(&context.module_path(def.service_name()), type_);
}
root
}
fn write_cargo_toml(
&self,
dir: &Path,
info: &CrateInfo,
def: &ConjureDefinition,
) -> Result<(), Error> {
fs::create_dir_all(dir)
.with_context(|_| format!("error creating directory {}", dir.display()))?;
let metadata = def
.extensions()
.get("recommended-product-dependencies")
.map(|deps| cargo_toml::Metadata {
sls: cargo_toml::Sls {
recommended_product_dependencies: deps,
},
});
let mut needs_object = false;
let mut needs_error = false;
let mut needs_http = false;
if !def.types().is_empty() {
needs_object = true;
}
if !def.errors().is_empty() {
needs_object = true;
needs_error = true;
}
if !def.services().is_empty() {
needs_http = true;
needs_object = true;
}
let conjure_version = env!("CARGO_PKG_VERSION");
let mut dependencies = BTreeMap::new();
if needs_object {
dependencies.insert("conjure-object", conjure_version);
}
if needs_error {
dependencies.insert("conjure-error", conjure_version);
}
if needs_http {
dependencies.insert("conjure-http", conjure_version);
}
let manifest = cargo_toml::Manifest {
package: cargo_toml::Package {
name: &info.name,
version: &info.version,
edition: "2018",
metadata,
},
dependencies,
};
let manifest = toml::to_string_pretty(&manifest).unwrap();
let file = dir.join("Cargo.toml");
fs::write(&file, &manifest)
.with_context(|_| format!("error writing manifest file {}", file.display()))?;
Ok(())
}
fn write_rustfmt_toml(&self, dir: &Path) -> Result<(), Error> {
let contents = "\
disable_all_formatting = true
";
let file = dir.join("rustfmt.toml");
fs::write(&file, contents).with_context(|_| "error writing rustfmt.toml")?;
Ok(())
}
}
struct Type {
module_name: String,
type_names: Vec<String>,
contents: TokenStream,
}
struct ModuleTrie {
submodules: BTreeMap<String, ModuleTrie>,
types: Vec<Type>,
}
impl ModuleTrie {
fn new() -> ModuleTrie {
ModuleTrie {
submodules: BTreeMap::new(),
types: vec![],
}
}
fn insert(&mut self, module_path: &[String], type_: Type) {
match module_path.split_first() {
Some((first, rest)) => self
.submodules
.entry(first.clone())
.or_insert_with(ModuleTrie::new)
.insert(rest, type_),
None => self.types.push(type_),
}
}
fn render(&self, dir: &Path, lib_root: bool) -> Result<(), Error> {
fs::create_dir_all(dir)
.with_context(|_| format!("error creating directory {}", dir.display()))?;
for type_ in &self.types {
self.write_module(
&dir.join(format!("{}.rs", type_.module_name)),
&type_.contents,
)?;
}
for (name, module) in &self.submodules {
module.render(&dir.join(name), false)?;
}
let root = self.create_root_module(lib_root);
let file_name = if lib_root { "lib.rs" } else { "mod.rs" };
self.write_module(&dir.join(file_name), &root)?;
Ok(())
}
fn write_module(&self, path: &Path, contents: &TokenStream) -> Result<(), Error> {
let file = syn::parse2(contents.clone())?;
let formatted = prettyplease::unparse(&file);
fs::write(path, &formatted)
.with_context(|_| format!("error writing module {}", path.display()))?;
Ok(())
}
fn create_root_module(&self, lib_root: bool) -> TokenStream {
let attrs = if lib_root {
quote! {
#![allow(warnings)]
}
} else {
quote! {}
};
let uses = self.types.iter().map(|m| {
let module_name = m.module_name.parse::<TokenStream>().unwrap();
let type_names = m
.type_names
.iter()
.map(|n| n.parse::<TokenStream>().unwrap());
quote! {
#[doc(inline)]
pub use self::#module_name::{#(#type_names),*};
}
});
let type_mods = self.types.iter().map(|m| {
let module_name = m.module_name.parse::<TokenStream>().unwrap();
quote! {
pub mod #module_name;
}
});
let sub_mods = self.submodules.keys().map(|v| {
let module_name = v.parse::<TokenStream>().unwrap();
quote! {
pub mod #module_name;
}
});
quote! {
#attrs
#(#uses)*
#(#type_mods)*
#(#sub_mods)*
}
}
}