use serde::Serialize;
use serde_json::Value;
use tera::Context;
use tera::Tera;
use crate::{discovery::Discovered, error::Error, tools};
use std::{collections::HashMap, fs::File, io::Write, path::PathBuf, process::Command};
use super::openapi::Openapi;
use inflector::Inflector;
#[derive(Debug)]
pub struct Templates {
pub list: Vec<Template>,
}
#[derive(Debug)]
pub enum Template {
Models(ModelsTemplate),
Endpoints(EndpointsTemplate),
Tags(TagsTemplate),
Static(StaticTemplate),
File(FileTemplate),
}
#[derive(Debug)]
pub struct EndpointsTemplate {
relative: PathBuf,
filename: Filename,
content_type: String,
condition: Option<Condition>,
group_by: GroupBy,
}
#[derive(Debug)]
pub struct TagsTemplate {
relative: PathBuf,
filename: Filename,
content_type: String,
condition: Option<Condition>,
}
#[derive(Debug)]
pub struct ModelsTemplate {
relative: PathBuf,
filename: Filename,
condition: Option<Condition>,
}
#[derive(Debug)]
pub struct StaticTemplate {
relative: PathBuf,
filename: Filename,
condition: Option<Condition>,
}
#[derive(Debug)]
pub struct Condition {
pub kv: String,
}
#[derive(Debug, Default)]
pub struct GroupBy {
pub kind: Option<String>,
}
#[derive(Serialize)]
pub struct TagContainer {
tag: String,
endpoints: Vec<super::openapi::endpoint::Endpoint>,
}
pub trait Group {
fn process(&self, openapi: &mut Openapi, container: &mut super::CodegenContainer);
}
#[derive(Debug)]
pub struct FileTemplate {
relative: String,
path: PathBuf,
}
#[derive(Eq, PartialEq, Debug)]
pub enum TemplateType {
Models,
Endpoints,
}
#[derive(Debug, Clone)]
pub struct Filename {
filename: String,
}
impl Filename {
pub fn from(filename: String) -> Self {
Self { filename }
}
pub fn resolve(&self, container: &super::CodegenContainer) -> Result<String, Error> {
tools::fill_parameters(&self.filename, container)
}
}
impl Condition {
pub fn from(kv: &str) -> Result<Self, Error> {
Ok(Self { kv: kv.to_string() })
}
pub fn check(&self, container: &super::CodegenContainer) -> bool {
tools::fill_parameters(&self.kv, container)
.map(|s| {
let parts = s.split(':').collect::<Vec<&str>>();
if let [left, right] = parts[..] {
left == right
} else {
true
}
})
.unwrap_or(false)
}
}
impl GroupBy {
pub fn from(group_by: &str) -> Result<Self, Error> {
if group_by != "tag" {
Err(Error::CodegenNotAllowedGroupBy(group_by.to_string()))
} else {
Ok(Self {
kind: Some(group_by.to_string()),
})
}
}
pub fn split(&self, openapi: &Openapi) -> impl IntoIterator<Item = impl Group> {
match &self.kind {
Some(_) => TagGroup::produce(openapi)
.into_iter()
.map(GroupType::TagGroup)
.collect::<Vec<_>>(),
None => vec![GroupType::NoGroup],
}
}
}
pub struct TagGroup {
tag: String,
}
impl Group for TagGroup {
fn process(&self, openapi: &mut Openapi, container: &mut super::CodegenContainer) {
container.data.insert(
"tag".to_string(),
Value::String(self.tag.clone().to_pascal_case()),
);
openapi
.endpoints
.retain(|s| s.get_tags().contains(&self.tag));
}
}
impl TagGroup {
pub fn produce(openapi: &Openapi) -> Vec<TagGroup> {
let mut tags = openapi.endpoints.iter().fold(vec![], |mut acc, item| {
acc.append(&mut item.get_tags().clone());
acc
});
tags.sort();
tags.dedup();
tags.iter()
.map(|t| TagGroup { tag: t.clone() })
.collect::<Vec<_>>()
}
pub fn filter(
&self,
endpoints: &[super::openapi::endpoint::Endpoint],
) -> Vec<super::openapi::endpoint::Endpoint> {
endpoints
.iter()
.filter(|e| e.get_tags().contains(&self.tag))
.cloned()
.collect()
}
}
pub enum GroupType {
TagGroup(TagGroup),
NoGroup,
}
impl Group for GroupType {
fn process(&self, openapi: &mut Openapi, container: &mut super::CodegenContainer) {
match &self {
Self::TagGroup(t) => t.process(openapi, container),
Self::NoGroup => {}
}
}
}
impl Templates {
pub fn includes(&self, types: &[TemplateType]) -> bool {
self.list
.iter()
.filter_map(|t| match *t {
Template::Models(_) => Some(TemplateType::Models),
Template::Endpoints(_) => Some(TemplateType::Endpoints),
_ => None,
})
.filter(|f| types.contains(f))
.count()
> 0
}
}
impl Template {
fn from_file(relative: String, path: PathBuf) -> Self {
Template::File(FileTemplate { relative, path })
}
fn from_content(relative: String, content: String) -> Result<Self, Error> {
let first = content.lines().next();
if let Some(line) = first {
let mut first_line = line.to_string();
let last_hash = first_line
.char_indices()
.find(|&(_, c)| c != '#')
.map_or(0, |(idx, _)| idx);
first_line = first_line[last_hash..].trim().to_string();
if !first_line.starts_with("{# ") {
return Err(Error::CodegenFileSkipped);
}
let params = super::format(first_line.trim_matches(&['{', '}', '#', ' '] as &[_]))?;
if let Some(serde_json::Value::String(min_version)) = params.get("min_version") {
let min = semver::Version::parse(min_version).map_err(Error::SemVersion)?;
let current = semver::Version::parse(crate::VERSION).unwrap();
let req = semver::VersionReq::parse(
format!(">={}, <{}.{}.0", min_version, min.major + 1, min.minor).as_str(),
)
.unwrap();
if !req.matches(¤t) {
return Err(Error::IncorrectVersionError(current, min, relative));
}
} else {
return Err(Error::MissingMinVersionError(relative));
}
params
.get("type")
.ok_or_else(|| Error::CodegenFileHeaderRequired("type".to_string()))?
.as_str()
.map(|type_| match type_ {
"endpoints" => EndpointsTemplate::from(PathBuf::from(relative), ¶ms),
"models" => ModelsTemplate::from(PathBuf::from(relative), ¶ms),
"tags" => TagsTemplate::from(PathBuf::from(relative), ¶ms),
"static" => StaticTemplate::from(PathBuf::from(relative), ¶ms),
_ => Err(Error::CodegenFileHeaderRequired("type".to_string())),
})
.unwrap()
} else {
Err(Error::CodegenFileSkipped)
}
}
pub fn format(&self, command: &str, files: Vec<String>) -> Result<(), Error> {
let parts = crate::tools::ArgumentsExtractor::new(command).collect::<Vec<String>>();
for file in files {
let mut cmd = Command::new(parts.first().unwrap());
for i in 1..parts.len() {
cmd.arg(parts.get(i).unwrap());
}
let output = cmd
.arg(file)
.output()
.map_err(Error::CodegenFormattingError)?;
if !output.status.success() {
return Err(Error::CodegenFormattingCommandError(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
}
Ok(())
}
}
impl StaticTemplate {
pub fn from(relative: PathBuf, config: &HashMap<&str, Value>) -> Result<Template, Error> {
let filename = Filename::from(
config
.get("filename")
.ok_or_else(|| Error::CodegenFileHeaderRequired("filename".to_string()))?
.as_str()
.unwrap()
.to_string(),
);
let condition = config
.get("if")
.map(|s| Condition::from(s.as_str().unwrap()))
.map_or(Ok(None), |v| v.map(Some))?;
Ok(Template::Static(Self {
relative,
filename,
condition,
}))
}
pub fn render(
&self,
tera: &Tera,
target_dir: &str,
container: &super::CodegenContainer,
) -> Result<Vec<String>, Error> {
if self
.condition
.as_ref()
.map(|s| s.check(container))
.unwrap_or(true)
{
process_render(
tera,
serde_json::json!({}),
PathBuf::from(format!(
"{}/{}",
target_dir,
self.filename.resolve(container)?
)),
self.relative.clone(),
container,
)
} else {
log::info!("Template skipped due to condition: {:?}", self.relative);
Ok(vec![])
}
}
}
impl EndpointsTemplate {
pub fn from(relative: PathBuf, config: &HashMap<&str, Value>) -> Result<Template, Error> {
let filename = Filename::from(
config
.get("filename")
.ok_or_else(|| Error::CodegenFileHeaderRequired("filename".to_string()))?
.as_str()
.unwrap()
.to_string(),
);
let content_type = config
.get("content_type")
.map(|s| s.as_str().unwrap().to_string())
.unwrap_or_else(|| "application/json".to_string());
let condition = config
.get("if")
.map(|s| Condition::from(s.as_str().unwrap()))
.map_or(Ok(None), |v| v.map(Some))?;
let group_by = config
.get("group_by")
.map(|s| GroupBy::from(s.as_str().unwrap()))
.unwrap_or_else(|| Ok(GroupBy::default()))?;
Ok(Template::Endpoints(Self {
relative,
filename,
content_type,
condition,
group_by,
}))
}
pub fn render(
&self,
tera: &Tera,
target_dir: &str,
openapi: &super::openapi::Openapi,
container: &super::CodegenContainer,
) -> Result<Vec<String>, Error> {
let mut result = vec![];
for group in self.group_by.split(openapi) {
let mut openapi = openapi.clone().set_content_type(&self.content_type);
let mut container = container.clone();
container.data.insert(
"formats".to_string(),
serde_json::to_value(openapi.models.formats()).unwrap(),
);
group.process(&mut openapi, &mut container);
if self
.condition
.as_ref()
.map(|s| s.check(&container))
.unwrap_or(true)
{
result.append(&mut process_render(
tera,
openapi,
PathBuf::from(format!(
"{}/{}",
target_dir,
self.filename.resolve(&container)?
)),
self.relative.clone(),
&container,
)?)
} else {
log::info!("Template skipped due to condition: {:?}", self.relative);
}
}
Ok(result)
}
}
impl TagsTemplate {
pub fn from(relative: PathBuf, config: &HashMap<&str, Value>) -> Result<Template, Error> {
let filename = Filename::from(
config
.get("filename")
.ok_or_else(|| Error::CodegenFileHeaderRequired("filename".to_string()))?
.as_str()
.unwrap()
.to_string(),
);
let content_type = config
.get("content_type")
.map(|s| s.as_str().unwrap().to_string())
.unwrap_or_else(|| "application/json".to_string());
let condition = config
.get("if")
.map(|s| Condition::from(s.as_str().unwrap()))
.map_or(Ok(None), |v| v.map(Some))?;
Ok(Template::Tags(Self {
relative,
filename,
content_type,
condition,
}))
}
pub fn render(
&self,
tera: &Tera,
target_dir: &str,
openapi: &super::openapi::Openapi,
container: &super::CodegenContainer,
) -> Result<Vec<String>, Error> {
let groups = TagGroup::produce(openapi);
let mut tags: Vec<TagContainer> = vec![];
let mut processed = openapi.clone().set_content_type(&self.content_type);
let mut container = container.clone();
for group in groups {
tags.push(TagContainer {
tag: group.tag.clone().to_pascal_case(),
endpoints: group.filter(&openapi.endpoints),
})
}
processed.endpoints = vec![];
container
.data
.insert("tags".to_string(), serde_json::to_value(tags).unwrap());
container.data.insert(
"formats".to_string(),
serde_json::to_value(openapi.models.formats()).unwrap(),
);
if self
.condition
.as_ref()
.map(|s| s.check(&container))
.unwrap_or(true)
{
process_render(
tera,
processed,
PathBuf::from(format!(
"{}/{}",
target_dir,
self.filename.resolve(&container)?
)),
self.relative.clone(),
&container,
)
} else {
log::info!("Template skipped due to condition: {:?}", self.relative);
Ok(vec![])
}
}
}
impl ModelsTemplate {
pub fn from(relative: PathBuf, config: &HashMap<&str, Value>) -> Result<Template, Error> {
let filename = Filename::from(
config
.get("filename")
.ok_or_else(|| Error::CodegenFileHeaderRequired("filename".to_string()))?
.as_str()
.unwrap()
.to_string(),
);
let condition = config
.get("if")
.map(|s| Condition::from(s.as_str().unwrap()))
.map_or(Ok(None), |v| v.map(Some))?;
Ok(Template::Models(Self {
relative,
filename,
condition,
}))
}
pub fn render(
&self,
tera: &Tera,
target_dir: &str,
models: &super::jsonschema::ModelContainer,
container: &super::CodegenContainer,
) -> Result<Vec<String>, Error> {
if self
.condition
.as_ref()
.map(|s| s.check(container))
.unwrap_or(true)
{
process_render(
tera,
models,
PathBuf::from(format!(
"{}/{}",
target_dir,
self.filename.resolve(container)?
)),
self.relative.clone(),
container,
)
} else {
log::info!("Template skipped due to condition: {:?}", self.relative);
Ok(vec![])
}
}
}
impl FileTemplate {
pub fn copy(&self, target_dir: &str) -> Result<Vec<String>, Error> {
let target = PathBuf::from(format!("{}/{}", target_dir, self.relative));
log::info!("Copying: {:?}", target);
let mut directory = target.clone();
directory.pop();
std::fs::create_dir_all(directory).map_err(|e| Error::CodegenFileError(e.to_string()))?;
std::fs::copy(self.path.clone(), target.clone())
.map(|_| ())
.map_err(|e| Error::CodegenFileError(e.to_string()))?;
Ok(vec![target.to_string_lossy().to_string()])
}
}
pub fn get(discovered: Discovered) -> Result<Templates, Error> {
let mut list: Vec<Template> = vec![];
for (relative, content) in discovered.templates {
let result = Template::from_content(relative.clone(), content);
match result {
Ok(template) => {
list.push(template);
}
Err(err) => match err {
Error::CodegenFileSkipped => {
log::trace!("file skipped: {}", relative);
continue;
}
e => return Err(e),
},
}
}
for (relative, path) in discovered.files {
list.push(Template::from_file(relative, path))
}
if list.is_empty() {
return Err(Error::CodegenTemplatesDirectoryError);
}
Ok(Templates { list })
}
fn process_render(
tera: &Tera,
data: impl Serialize + Clone,
target: PathBuf,
relative: PathBuf,
container: &super::CodegenContainer,
) -> Result<Vec<String>, Error> {
let mut ctx = Context::from_serialize(serde_json::to_value(data).unwrap()).unwrap();
let data = serde_json::to_value(container).unwrap();
for (key, value) in data.as_object().unwrap() {
ctx.insert(key, value);
}
let result = tera
.render(&relative.to_string_lossy(), &ctx)
.map_err(Error::CodegenTemplateError)?;
if result.trim().is_empty() {
return Ok(vec![]);
}
log::info!("Rendering: {:?}", target);
let mut directory = target.clone();
directory.pop();
std::fs::create_dir_all(directory).map_err(|e| Error::CodegenFileError(e.to_string()))?;
let mut file =
File::create(target.clone()).map_err(|e| Error::CodegenFileError(e.to_string()))?;
file.write_all(result.as_bytes())
.map_err(|e| Error::CodegenFileError(e.to_string()))?;
Ok(vec![target.to_string_lossy().to_string()])
}