#![warn(clippy::all)]
#![warn(clippy::all, missing_docs)]
#![doc(html_root_url = "https://docs.rs/conjure-codegen/0.3")]
#![recursion_limit = "256"]
use failure::{bail, Error, ResultExt};
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::BTreeMap;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::Path;
use std::process::Command;
use crate::context::Context;
use crate::types::{ConjureDefinition, TypeDefinition};
mod aliases;
mod clients;
mod context;
mod enums;
mod errors;
mod objects;
#[allow(dead_code, clippy::all)]
#[rustfmt::skip] mod types;
mod servers;
mod unions;
#[cfg(feature = "example-types")]
#[allow(warnings)]
#[rustfmt::skip] pub mod example_types;
pub struct Config {
rustfmt: OsString,
run_rustfmt: bool,
exhaustive: bool,
strip_prefix: Option<String>,
}
impl Default for Config {
fn default() -> Config {
Config::new()
}
}
impl Config {
pub fn new() -> Config {
Config {
rustfmt: env::var_os("RUSTFMT").unwrap_or_else(|| OsString::from("rustfmt")),
run_rustfmt: true,
exhaustive: false,
strip_prefix: None,
}
}
pub fn exhaustive(&mut self, exhaustive: bool) -> &mut Config {
self.exhaustive = exhaustive;
self
}
pub fn run_rustfmt(&mut self, run_rustfmt: bool) -> &mut Config {
self.run_rustfmt = run_rustfmt;
self
}
pub fn rustfmt<T>(&mut self, rustfmt: T) -> &mut Config
where
T: AsRef<OsStr>,
{
self.rustfmt = rustfmt.as_ref().to_owned();
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 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);
modules.render(self, out_dir)?;
if self.run_rustfmt {
let _ = Command::new(&self.rustfmt)
.arg(&out_dir.join("mod.rs"))
.status();
}
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::server_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.strip_prefix.as_ref().map(|s| &**s),
);
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()),
context.type_name(def.service_name().name()).to_string(),
format!("{}Resource", def.service_name().name()),
],
contents,
};
root.insert(&context.module_path(def.service_name()), type_);
}
root
}
}
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, config: &Config, dir: &Path) -> 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(config, &dir.join(name))?;
}
let root = self.create_root_module();
self.write_module(&dir.join("mod.rs"), &root)?;
Ok(())
}
fn write_module(&self, path: &Path, contents: &TokenStream) -> Result<(), Error> {
fs::write(path, contents.to_string())
.with_context(|_| format!("error writing module {}", path.display()))?;
Ok(())
}
fn create_root_module(&self) -> TokenStream {
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! {
#(#uses)*
#(#type_mods)*
#(#sub_mods)*
}
}
}