#![doc = include_str!("../README.md")]
#![allow(clippy::collapsible_if)]
pub mod config;
pub mod doc_parser;
pub mod dsl;
pub mod error;
pub mod generics;
pub mod index;
pub mod merger;
pub mod preprocessor;
pub mod scanner;
pub mod type_mapper;
pub mod visitor;
use config::Config;
use error::Result;
use std::path::PathBuf;
#[derive(Default)]
pub struct Generator {
inputs: Vec<PathBuf>,
includes: Vec<PathBuf>,
outputs: Vec<PathBuf>,
schema_outputs: Vec<PathBuf>,
path_outputs: Vec<PathBuf>,
fragment_outputs: Vec<PathBuf>,
}
impl Generator {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(mut self, config: Config) -> Self {
if let Some(inputs) = config.input {
self.inputs.extend(inputs);
}
if let Some(includes) = config.include {
self.includes.extend(includes);
}
if let Some(output) = config.output {
self.outputs.extend(output);
}
if let Some(output_schemas) = config.output_schemas {
self.schema_outputs.extend(output_schemas);
}
if let Some(output_paths) = config.output_paths {
self.path_outputs.extend(output_paths);
}
if let Some(output_fragments) = config.output_fragments {
self.fragment_outputs.extend(output_fragments);
}
self
}
pub fn input<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.inputs.push(path.into());
self
}
pub fn include<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.includes.push(path.into());
self
}
pub fn output<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.outputs.push(path.into());
self
}
pub fn output_schemas<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.schema_outputs.push(path.into());
self
}
pub fn output_paths<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.path_outputs.push(path.into());
self
}
pub fn output_fragments<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.fragment_outputs.push(path.into());
self
}
pub fn generate(self) -> Result<()> {
if self.outputs.is_empty()
&& self.schema_outputs.is_empty()
&& self.path_outputs.is_empty()
&& self.fragment_outputs.is_empty()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"At least one output path (output, output_schemas, output_paths, or output_fragments) is required",
)
.into());
}
log::info!(
"Scanning directories: {:?} and includes: {:?}",
self.inputs,
self.includes
);
let (snippets, registry) = scanner::scan_directories(&self.inputs, &self.includes)?;
log::info!("Merging {} snippets", snippets.len());
let mut merged_value = merger::merge_openapi(snippets)?;
if let serde_yaml_ng::Value::Mapping(ref mut root_map) = merged_value {
if let Some(serde_yaml_ng::Value::Mapping(comp_map)) =
root_map.get_mut(serde_yaml_ng::Value::String("components".to_string()))
{
comp_map.remove(serde_yaml_ng::Value::String(
"x-oas-forge-templates".to_string(),
));
comp_map.remove(serde_yaml_ng::Value::String(
"x-oas-forge-fragments".to_string(),
));
if comp_map.is_empty() {
root_map.remove(serde_yaml_ng::Value::String("components".to_string()));
}
}
}
if !self.outputs.is_empty() {
if let serde_yaml_ng::Value::Mapping(map) = &merged_value {
let openapi_key = serde_yaml_ng::Value::String("openapi".to_string());
let info_key = serde_yaml_ng::Value::String("info".to_string());
if !map.contains_key(&openapi_key) || !map.contains_key(&info_key) {
return Err(error::Error::NoRootFound);
}
} else {
return Err(error::Error::NoRootFound);
}
for output in &self.outputs {
self.write_file(output, &merged_value)?;
log::info!("Written full spec to {:?}", output);
}
}
if !self.schema_outputs.is_empty() {
let schemas = merged_value
.get("components")
.and_then(|c| c.get("schemas"))
.cloned()
.unwrap_or_else(|| serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new()));
if let serde_yaml_ng::Value::Mapping(m) = &schemas {
if m.is_empty() {
log::warn!("Generating empty schemas file.");
}
}
for output in &self.schema_outputs {
self.write_file(output, &schemas)?;
log::info!("Written schemas to {:?}", output);
}
}
if !self.path_outputs.is_empty() {
let paths = merged_value
.get("paths")
.cloned()
.unwrap_or_else(|| serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new()));
if let serde_yaml_ng::Value::Mapping(m) = &paths {
if m.is_empty() {
log::warn!("Generating empty paths file.");
}
}
for output in &self.path_outputs {
self.write_file(output, &paths)?;
log::info!("Written paths to {:?}", output);
}
}
if !self.fragment_outputs.is_empty() {
let mut fragment = merged_value.clone();
if let serde_yaml_ng::Value::Mapping(ref mut map) = fragment {
map.remove(serde_yaml_ng::Value::String("openapi".to_string()));
map.remove(serde_yaml_ng::Value::String("info".to_string()));
map.remove(serde_yaml_ng::Value::String("servers".to_string()));
}
if !registry.blueprints.is_empty() || !registry.fragments.is_empty() {
if let serde_yaml_ng::Value::Mapping(ref mut root_map) = fragment {
let components_key = serde_yaml_ng::Value::String("components".to_string());
let components = root_map.entry(components_key).or_insert_with(|| {
serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new())
});
if let serde_yaml_ng::Value::Mapping(comp_map) = components {
if !registry.blueprints.is_empty() {
if let Ok(val) = serde_json::to_value(®istry.blueprints) {
if let Ok(yaml_val) = serde_yaml_ng::to_value(&val) {
comp_map.insert(
serde_yaml_ng::Value::String(
"x-oas-forge-templates".to_string(),
),
yaml_val,
);
}
}
}
if !registry.fragments.is_empty() {
if let Ok(val) = serde_json::to_value(®istry.fragments) {
if let Ok(yaml_val) = serde_yaml_ng::to_value(&val) {
comp_map.insert(
serde_yaml_ng::Value::String(
"x-oas-forge-fragments".to_string(),
),
yaml_val,
);
}
}
}
}
}
}
for output in &self.fragment_outputs {
self.write_file(output, &fragment)?;
log::info!("Written fragment to {:?}", output);
}
}
Ok(())
}
fn write_file<T: serde::Serialize>(&self, path: &PathBuf, content: &T) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(path)?;
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("yaml");
match extension {
"json" => {
serde_json::to_writer_pretty(file, content)?;
}
"yaml" | "yml" => {
serde_yaml_ng::to_writer(file, content)?;
}
_ => {
serde_yaml_ng::to_writer(file, content)?;
}
}
Ok(())
}
}