mod filter;
pub mod openapi_loader;
pub use openapi_loader::openapi_load;
pub use openapi_loader::openapi_parse_str;
mod schema_cache;
pub mod templates {
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
}
use openapiv3::OpenAPI;
use std::sync::{Arc, Mutex};
pub fn environment(mut spec: OpenAPI) -> Result<minijinja::Environment<'static>, minijinja::Error> {
if spec.servers.is_empty() {
spec.servers.push(openapiv3::Server {
url: "/api".to_string(),
description: Some("Default server added by mandolin".to_string()),
..Default::default()
});
}
let value = serde_json::to_value(&spec).unwrap();
let mut env = minijinja::Environment::new();
for [k, v] in templates::TEMPLATES {
env.add_template(k, v)?;
}
env.add_global("spec", minijinja::Value::from_serialize(&value));
env.add_filter("to_snake_case", filter::to_snake_case);
env.add_filter("to_pascal_case", filter::to_pascal_case);
env.add_filter("to_camel_case", filter::to_camel_case);
env.add_filter("re_replace", filter::re_replace);
env.add_filter("encode", filter::encode);
env.add_filter("decode", filter::decode);
env.add_filter("ref_name", filter::ref_name);
{
let spec_value = value.clone();
env.add_filter(
"include_pointer",
move |pointer: &str| -> Result<minijinja::Value, minijinja::Error> {
let path = pointer.strip_prefix("#/").ok_or_else(|| {
minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!("invalid pointer: {pointer}"),
)
})?;
let mut current = &spec_value;
for segment in path.split('/') {
let decoded = segment.replace("~1", "/").replace("~0", "~");
current = current.get(&decoded).ok_or_else(|| {
minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!("pointer not found: {pointer}"),
)
})?;
}
Ok(minijinja::Value::from_serialize(current))
},
);
}
{
let spec_value = value.clone();
env.add_filter(
"deref",
move |v: minijinja::Value| -> Result<minijinja::Value, minijinja::Error> {
if let Ok(ref_val) = v.get_item(&minijinja::Value::from("$ref")) {
if let Some(ref_path) = ref_val.as_str() {
let path = ref_path.strip_prefix("#/").unwrap_or(ref_path);
let mut cur = &spec_value;
for seg in path.split('/') {
let decoded = seg.replace("~1", "/").replace("~0", "~");
cur = cur.get(&decoded).ok_or_else(|| {
minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!("deref: not found: {ref_path}"),
)
})?;
}
return Ok(minijinja::Value::from_serialize(cur));
}
}
Ok(v)
},
);
}
let cache = schema_cache::SchemaCache::new();
{
let c = cache.clone();
env.add_function(
"schema_push",
move |pointer: &str, content: Option<&str>| c.push(pointer, content),
);
let c = cache.clone();
env.add_function("schema_drain", move || c.drain());
}
{
let spec_value = value.clone();
env.add_function("anyof_tag", move |schema: minijinja::Value| -> String {
let any_of = match schema.get_item(&minijinja::Value::from("anyOf")) {
Ok(v) if !v.is_undefined() && !v.is_none() => v,
_ => return String::new(),
};
let mut disc_prop: Option<String> = None;
for i in 0.. {
let item = match any_of.get_item(&minijinja::Value::from(i)) {
Ok(v) if !v.is_undefined() => v,
_ => break,
};
let ref_path = match item.get_item(&minijinja::Value::from("$ref")) {
Ok(v) => match v.as_str() {
Some(s) => s.to_string(),
None => return String::new(),
},
_ => return String::new(),
};
let path = match ref_path.strip_prefix("#/") {
Some(p) => p,
None => return String::new(),
};
let mut current = &spec_value;
for segment in path.split('/') {
let decoded = segment.replace("~1", "/").replace("~0", "~");
current = match current.get(&decoded) {
Some(v) => v,
None => return String::new(),
};
}
let properties = match current.get("properties") {
Some(serde_json::Value::Object(m)) => m,
_ => return String::new(),
};
let mut found = None;
for (k, v) in properties {
if let Some(arr) = v.get("enum").and_then(|e| e.as_array()) {
if arr.len() == 1 {
found = Some(k.clone());
break;
}
}
}
match (&disc_prop, found) {
(None, Some(prop)) => disc_prop = Some(prop),
(Some(existing), Some(ref prop)) if existing == prop => {}
_ => return String::new(),
}
}
disc_prop.unwrap_or_default()
});
}
{
let skip_set: Arc<Mutex<std::collections::HashSet<(String, String)>>> =
Arc::new(Mutex::new(std::collections::HashSet::new()));
let s = skip_set.clone();
env.add_function(
"tag_skip_push",
move |ref_path: &str, prop_name: &str| -> bool {
s.lock()
.unwrap()
.insert((ref_path.to_string(), prop_name.to_string()));
true
},
);
let s = skip_set.clone();
env.add_function(
"tag_skip_get",
move |pointer: &str, prop_name: &str| -> bool {
s.lock()
.unwrap()
.contains(&(pointer.to_string(), prop_name.to_string()))
},
);
}
Ok(env)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use std::path::Path;
fn api_map() -> std::collections::HashMap<String, OpenAPI> {
fs::read_dir(Path::new(".").join("openapi"))
.unwrap()
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
let name = path.file_stem()?.to_str()?.to_string();
let f = fs::File::open(&path).ok()?;
crate::openapi_loader::openapi_load(std::io::BufReader::new(f))
.ok()
.map(|api| (name, api))
})
.collect()
}
fn render_target(template: &str, extension: &str) {
for (name, api) in api_map() {
println!("render start: {name}");
let env = environment(api).unwrap();
let tmpl = env.get_template(template).unwrap();
let output = tmpl.render(0).unwrap();
let out_path = format!("examples/{name}.{extension}");
if let Some(parent) = Path::new(&out_path).parent() {
fs::create_dir_all(parent).unwrap();
}
let mut writer = std::io::BufWriter::new(fs::File::create(&out_path).unwrap());
writeln!(writer, "{}", output).unwrap();
println!("render complete: {name}");
}
}
#[test]
fn render() {
render_target("RUST_AXUM", "rs");
render_target("TYPESCRIPT_HONO", "ts");
}
#[test]
fn call_block_supported() {
let mut env = minijinja::Environment::new();
env.add_template("test", r#"
{%- macro foreach(items) %}
{%- for item in items %}
{{ caller(item) }}
{%- endfor %}
{%- endmacro %}
{%- call(item) foreach(["a","b","c"]) %}[{{item}}]{%- endcall %}
"#).unwrap();
let result = env.get_template("test").unwrap().render(0).unwrap();
assert!(result.contains("[a]"), "call block not working: {result}");
assert!(result.contains("[b]"), "call block not working: {result}");
assert!(result.contains("[c]"), "call block not working: {result}");
}
}