use std::collections::HashMap;
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::RootSchema,
schema::Schema,
JsonSchema, Map,
};
use serde::Serialize;
pub use schemars;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(Debug, Clone)]
struct OpenApiPlaceHolderError(String);
impl std::fmt::Display for OpenApiPlaceHolderError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "OpenApiPlaceHolderError: {}", self.0)
}
}
impl std::error::Error for OpenApiPlaceHolderError {}
impl OpenApiPlaceHolderError {
pub fn new(message: &str) -> Self {
OpenApiPlaceHolderError(message.to_string())
}
}
pub struct OpenApiPlaceHolderBuilder {
pub schema_settings: SchemaSettings,
}
pub struct OpenApiPlaceHolder {
ph: HashMap<String, String>,
definitions: Map<String, Schema>,
schema_generator: SchemaGenerator,
}
impl Default for OpenApiPlaceHolderBuilder {
fn default() -> Self {
Self::new()
}
}
impl OpenApiPlaceHolderBuilder {
pub fn new() -> Self {
Self {
schema_settings: SchemaSettings::draft07().with(|s| {
s.option_nullable = true;
s.option_add_null_type = false;
}),
}
}
pub fn with_schema_settings(self, schema_settings: SchemaSettings) -> Self {
Self { schema_settings }
}
pub fn build(self) -> OpenApiPlaceHolder {
OpenApiPlaceHolder {
ph: HashMap::new(),
definitions: Map::new(),
schema_generator: self.schema_settings.into_generator(),
}
}
}
impl OpenApiPlaceHolder {
pub fn builder() -> OpenApiPlaceHolderBuilder {
OpenApiPlaceHolderBuilder::new()
}
pub fn new() -> Self {
Self::builder().build()
}
fn describe_struct<T: JsonSchema>(&mut self) -> RootSchema {
let root_schema = self.schema_generator.clone().into_root_schema_for::<T>();
for (k, v) in root_schema.clone().definitions {
self.definitions.insert(k, v);
}
root_schema
}
fn to_yaml(schema: &impl Serialize) -> Result<String> {
Ok(serde_yaml::to_string(schema)?
.replace("---\n", "")
.trim()
.to_owned())
}
pub fn substitute(mut self, name: &str, value: &str) -> Self {
self.ph.insert(name.to_owned(), value.to_owned());
self
}
pub fn query_params<T: JsonSchema>(mut self, name: &str) -> Result<Self> {
let root_schema = self.describe_struct::<T>();
let parameters = match root_schema.schema.object {
Some(object) => {
let mut result: Vec<String> = Vec::new();
for (name, schema) in object.properties.iter() {
let (required, description) = match schema {
Schema::Object(o) => (
if o.extensions.get("nullable").is_some() {
"false"
} else {
"true"
},
if let Some(metadata) = o.metadata.as_ref() {
metadata.description.as_ref()
} else {
None
},
),
_ => ("false", None),
};
let description_entry = if let Some(desc) = description {
format!(" description: {}\n", desc.trim()).to_string()
} else {
"".to_string()
};
let schema = Self::to_yaml(&schema)?
.split('\n')
.filter(|line| !line.contains("description:"))
.collect::<Vec<&str>>()
.join("\n");
result.push(format!(
"- in: query\n name: {name}\n{description} required: {required}\n schema:\n {schema}",
name=name,
required=required,
description=description_entry,
schema=Self::with_indent(" ", &schema),
))
}
result.join("\n")
}
_ => {
return Err(Box::new(OpenApiPlaceHolderError::new(
"Only object type supported",
)))
}
};
self.ph.insert(name.to_owned(), parameters);
Ok(self)
}
pub fn schema<T: JsonSchema>(mut self, name: &str) -> Result<Self> {
let root_schema = self.describe_struct::<T>();
self.ph
.insert(name.to_owned(), Self::to_yaml(&root_schema.schema)?);
Ok(self)
}
fn with_indent(indent: &str, value: &str) -> String {
value
.split('\n')
.enumerate()
.map(|(index, line)| {
if index == 0 {
return line.to_owned();
}
format!("{}{}", indent, line)
})
.collect::<Vec<String>>()
.join("\n")
}
pub fn render_to(mut self, template: &str) -> Result<String> {
let mut result = template.to_owned();
self.ph.insert(
"oaph::definitions".to_string(),
if !self.definitions.is_empty() {
Self::to_yaml(&self.definitions)?
} else {
"".to_owned()
},
);
for (k, v) in self.ph.iter() {
result = result
.split('\n')
.map(|line| {
let pattern = format!("{{{{{}}}}}", k);
if let Some(position) = line.find(&pattern) {
let (indent, _) = line.split_at(position);
line.replace(&pattern, &Self::with_indent(indent, v))
.trim_end()
.to_owned()
} else {
line.to_owned()
}
})
.collect::<Vec<String>>()
.join("\n");
}
Ok(result)
}
pub fn swagger_ui_html(openapi_yaml_url: &str, title: &str) -> String {
include_str!("../misc/swagger-ui.html")
.replace("{{openapi_yaml_url}}", openapi_yaml_url)
.replace("{{title}}", title)
}
pub fn redoc_ui_html(openapi_yaml_url: &str, title: &str) -> String {
include_str!("../misc/redoc-ui.html")
.replace("{{openapi_yaml_url}}", openapi_yaml_url)
.replace("{{title}}", title)
}
pub fn render_to_file<P: AsRef<std::path::Path>>(self, template: &str, path: P) -> Result<()> {
std::fs::write(path, self.render_to(template)?)?;
Ok(())
}
pub fn swagger_ui_html_to_file<P: AsRef<std::path::Path>>(
openapi_yaml_url: &str,
title: &str,
path: P,
) -> Result<()> {
std::fs::write(path, Self::swagger_ui_html(openapi_yaml_url, title))?;
Ok(())
}
pub fn redoc_ui_html_to_file<P: AsRef<std::path::Path>>(
openapi_yaml_url: &str,
title: &str,
path: P,
) -> Result<()> {
std::fs::write(path, Self::redoc_ui_html(openapi_yaml_url, title))?;
Ok(())
}
}
impl Default for OpenApiPlaceHolder {
fn default() -> Self {
Self::new()
}
}
#[cfg(doctest)]
mod test_readme {
macro_rules! external_doc_test {
($x:expr) => {
#[doc = $x]
extern "C" {}
};
}
external_doc_test!(include_str!("../README.md"));
}