#![warn(clippy::all, missing_docs)]
#![allow(clippy::needless_doctest_main)]
#![recursion_limit = "256"]
use crate::context::Context;
use crate::merge_toml::left_merge;
use crate::types::objects::{ConjureDefinition, TypeDefinition};
use anyhow::{bail, Context as _, Error};
use context::BaseModule;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::Path;
use toml::Value;
mod aliases;
mod cargo_toml;
mod clients;
mod context;
mod enums;
mod errors;
mod merge_toml;
mod objects;
mod servers;
#[allow(dead_code, clippy::all)]
#[rustfmt::skip]
mod types;
mod human_size;
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,
serialize_empty_collections: bool,
use_legacy_error_serialization: bool,
public_fields: bool,
strip_prefix: Option<String>,
version: Option<String>,
build_crate: Option<CrateInfo>,
extra_manifest_config: Option<Value>,
}
impl Default for Config {
fn default() -> Config {
Config::new()
}
}
impl Config {
pub fn new() -> Config {
Config {
exhaustive: false,
serialize_empty_collections: false,
use_legacy_error_serialization: true,
public_fields: false,
strip_prefix: None,
version: None,
build_crate: None,
extra_manifest_config: None,
}
}
pub fn exhaustive(&mut self, exhaustive: bool) -> &mut Config {
self.exhaustive = exhaustive;
self
}
pub fn serialize_empty_collections(
&mut self,
serialize_empty_collections: bool,
) -> &mut Config {
self.serialize_empty_collections = serialize_empty_collections;
self
}
pub fn use_legacy_error_serialization(
&mut self,
use_legacy_error_serialization: bool,
) -> &mut Config {
self.use_legacy_error_serialization = use_legacy_error_serialization;
self
}
pub fn public_fields(&mut self, public_fields: bool) -> &mut Config {
self.public_fields = public_fields;
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 extra_manifest_config<T>(&mut self, config: T) -> &mut Config
where
T: Into<Option<toml::Table>>,
{
self.extra_manifest_config = config.into().map(Value::Table);
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.serialize_empty_collections,
self.use_legacy_error_serialization,
self.public_fields,
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, BaseModule::Objects, 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(BaseModule::Objects, 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(BaseModule::Errors, def.error_name()),
type_,
);
}
for def in defs.services() {
let client = clients::generate(&context, def);
let contents = quote! {
#client
};
let type_ = Type {
module_name: context.module_name(def.service_name()),
type_names: vec![
format!("{}", def.service_name().name()),
format!("{}Client", def.service_name().name()),
format!("Async{}", def.service_name().name()),
format!("Async{}Client", def.service_name().name()),
],
contents,
};
root.insert(
&context.module_path(BaseModule::Clients, def.service_name()),
type_,
);
let server = servers::generate(&context, def);
let contents = quote! {
#server
};
let type_ = Type {
module_name: context.module_name(def.service_name()),
type_names: vec![
context.type_name(def.service_name().name()).to_string(),
format!("Async{}", def.service_name().name()),
format!("{}Endpoints", def.service_name().name()),
format!("Async{}Endpoints", def.service_name().name()),
],
contents,
};
root.insert(
&context.module_path(BaseModule::Endpoints, def.service_name()),
type_,
);
}
root.deconflict();
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 = if let Some(extra_manifest_toml) = self.extra_manifest_config.as_ref() {
let mut manifest_toml = toml::Value::Table(toml::Table::try_from(&manifest)?);
left_merge(&mut manifest_toml, extra_manifest_toml)?;
toml::to_string_pretty(&manifest_toml).unwrap()
} else {
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 deconflict(&mut self) {
for type_ in &mut self.types {
if self.submodules.contains_key(&type_.module_name) {
type_.module_name.push('_');
}
}
for submodule in self.submodules.values_mut() {
submodule.deconflict();
}
}
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)*
}
}
}