torn-api.rs/torn-api-codegen/src/model/parameter.rs

446 lines
14 KiB
Rust

use std::fmt::Write;
use heck::ToUpperCamelCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use crate::openapi::parameter::{
OpenApiParameter, OpenApiParameterDefault, OpenApiParameterSchema,
ParameterLocation as SchemaLocation,
};
use super::r#enum::Enum;
#[derive(Debug, Clone)]
pub struct ParameterOptions<P> {
pub default: Option<P>,
pub minimum: Option<P>,
pub maximum: Option<P>,
}
#[derive(Debug, Clone)]
pub enum ParameterType {
I32 {
options: ParameterOptions<i32>,
},
String,
Boolean,
Enum {
options: ParameterOptions<String>,
r#type: Enum,
},
Schema {
type_name: String,
},
Array {
items: Box<ParameterType>,
},
}
impl ParameterType {
pub fn from_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
match schema {
OpenApiParameterSchema {
r#type: Some("integer"),
// BUG: missing for some types in the spec
// format: Some("int32"),
..
} => {
let default = match schema.default {
Some(OpenApiParameterDefault::Int(d)) => Some(d),
None => None,
_ => return None,
};
Some(Self::I32 {
options: ParameterOptions {
default,
minimum: schema.minimum,
maximum: schema.maximum,
},
})
}
OpenApiParameterSchema {
r#type: Some("string"),
r#enum: Some(variants),
..
} if variants.as_slice() == ["true", "false"]
|| variants.as_slice() == ["false", "true "] =>
{
Some(ParameterType::Boolean)
}
OpenApiParameterSchema {
r#type: Some("string"),
r#enum: Some(_),
..
} => {
let default = match schema.default {
Some(OpenApiParameterDefault::Str(d)) => Some(d.to_owned()),
None => None,
_ => return None,
};
Some(ParameterType::Enum {
options: ParameterOptions {
default,
minimum: None,
maximum: None,
},
r#type: Enum::from_parameter_schema(name, schema)?,
})
}
OpenApiParameterSchema {
r#type: Some("string"),
..
} => Some(ParameterType::String),
OpenApiParameterSchema {
ref_path: Some(path),
..
} => {
let type_name = path.strip_prefix("#/components/schemas/")?.to_owned();
Some(ParameterType::Schema { type_name })
}
OpenApiParameterSchema {
r#type: Some("array"),
items: Some(items),
..
} => Some(Self::Array {
items: Box::new(Self::from_schema(name, items)?),
}),
_ => None,
}
}
pub fn codegen_type_name(&self, name: &str) -> TokenStream {
match self {
Self::I32 { .. } | Self::String | Self::Enum { .. } | Self::Array { .. } => {
format_ident!("{name}").into_token_stream()
}
Self::Boolean => quote! { bool },
Self::Schema { type_name } => {
let type_name = format_ident!("{type_name}",);
quote! { crate::models::#type_name }
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParameterLocation {
Query,
Path,
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub value: String,
pub description: Option<String>,
pub r#type: ParameterType,
pub required: bool,
pub location: ParameterLocation,
}
impl Parameter {
pub fn from_schema(name: &str, schema: &OpenApiParameter) -> Option<Self> {
let name = match name {
"From" => "FromTimestamp".to_owned(),
"To" => "ToTimestamp".to_owned(),
name => name.to_owned(),
};
let value = schema.name.to_owned();
let description = schema.description.as_deref().map(ToOwned::to_owned);
let location = match &schema.r#in {
SchemaLocation::Query => ParameterLocation::Query,
SchemaLocation::Path => ParameterLocation::Path,
};
let r#type = ParameterType::from_schema(&name, &schema.schema)?;
Some(Self {
name,
value,
description,
r#type,
required: schema.required,
location,
})
}
pub fn codegen(&self) -> Option<TokenStream> {
match &self.r#type {
ParameterType::I32 { options } => {
let name = format_ident!("{}", self.name);
let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
if options.default.is_some()
|| options.minimum.is_some()
|| options.maximum.is_some()
{
_ = writeln!(desc, "\n # Notes");
}
let constructor = if let (Some(min), Some(max)) = (options.minimum, options.maximum)
{
_ = write!(desc, "Values have to lie between {min} and {max}. ");
let name_raw = &self.name;
quote! {
impl #name {
pub fn new(inner: i32) -> Result<Self, crate::ParameterError> {
if inner > #max || inner < #min {
Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
} else {
Ok(Self(inner))
}
}
}
impl TryFrom<i32> for #name {
type Error = crate::ParameterError;
fn try_from(inner: i32) -> Result<Self, Self::Error> {
if inner > #max || inner < #min {
Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
} else {
Ok(Self(inner))
}
}
}
}
} else {
quote! {
impl #name {
pub fn new(inner: i32) -> Self {
Self(inner)
}
}
impl From<i32> for #name {
fn from(inner: i32) -> Self {
Self(inner)
}
}
}
};
if let Some(default) = options.default {
_ = write!(desc, "The default value is {default}.");
}
let doc = quote! {
#[doc = #desc]
};
Some(quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#doc
pub struct #name(i32);
#constructor
impl From<#name> for i32 {
fn from(value: #name) -> Self {
value.0
}
}
impl #name {
pub fn into_inner(self) -> i32 {
self.0
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
})
}
ParameterType::Enum { options, r#type } => {
let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
if let Some(default) = &options.default {
let default = default.to_upper_camel_case();
_ = write!(
desc,
r#"
# Notes
The default value [Self::{}](self::{}#variant.{})"#,
default, self.name, default
);
}
let doc = quote! { #[doc = #desc]};
let inner = r#type.codegen()?;
Some(quote! {
#doc
#inner
})
}
ParameterType::Array { items } => {
let (inner_name, outer_name) = match items.as_ref() {
ParameterType::I32 { .. }
| ParameterType::String
| ParameterType::Array { .. }
| ParameterType::Enum { .. } => self.name.strip_suffix('s').map_or_else(
|| (self.name.to_owned(), format!("{}s", self.name)),
|s| (s.to_owned(), self.name.to_owned()),
),
ParameterType::Boolean => ("bool".to_owned(), self.name.clone()),
ParameterType::Schema { type_name } => (type_name.clone(), self.name.clone()),
};
let inner = Self {
r#type: *items.clone(),
name: inner_name.clone(),
..self.clone()
};
let mut code = inner.codegen().unwrap_or_default();
let name = format_ident!("{}", outer_name);
let inner_ty = items.codegen_type_name(&inner_name);
code.extend(quote! {
#[derive(Debug, Clone)]
pub struct #name(pub Vec<#inner_ty>);
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut first = true;
for el in &self.0 {
if first {
first = false;
write!(f, "{el}")?;
} else {
write!(f, ",{el}")?;
}
}
Ok(())
}
}
impl<T> From<T> for #name where T: IntoIterator<Item = #inner_ty> {
fn from(value: T) -> #name {
let items = value.into_iter().collect();
Self(items)
}
}
});
Some(code)
}
_ => None,
}
}
}
#[cfg(test)]
mod test {
use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
use super::*;
#[test]
fn resolve_components() {
let schema = OpenApiSchema::read().unwrap();
let mut parameters = 0;
let mut unresolved = vec![];
for (name, desc) in &schema.components.parameters {
parameters += 1;
if Parameter::from_schema(name, desc).is_none() {
unresolved.push(name);
}
}
if !unresolved.is_empty() {
panic!(
"Failed to resolve {}/{} params. Could not resolve [{}]",
unresolved.len(),
parameters,
unresolved
.into_iter()
.map(|u| format!("`{u}`"))
.collect::<Vec<_>>()
.join(", ")
)
}
}
#[test]
fn resolve_inline() {
let schema = OpenApiSchema::read().unwrap();
let mut params = 0;
let mut unresolved = Vec::new();
for (path, body) in &schema.paths {
for param in &body.get.parameters {
if let OpenApiPathParameter::Inline(inline) = param {
params += 1;
if Parameter::from_schema(inline.name, inline).is_none() {
unresolved.push(format!("`{}.{}`", path, inline.name));
}
}
}
}
if !unresolved.is_empty() {
panic!(
"Failed to resolve {}/{} inline params. Could not resolve [{}]",
unresolved.len(),
params,
unresolved.join(", ")
)
}
}
#[test]
fn codegen_inline() {
let schema = OpenApiSchema::read().unwrap();
let mut params = 0;
let mut unresolved = Vec::new();
for (path, body) in &schema.paths {
for param in &body.get.parameters {
if let OpenApiPathParameter::Inline(inline) = param {
if inline.r#in == SchemaLocation::Query {
let Some(param) = Parameter::from_schema(inline.name, inline) else {
continue;
};
if matches!(
param.r#type,
ParameterType::Schema { .. }
| ParameterType::Boolean
| ParameterType::String
) {
continue;
}
params += 1;
if param.codegen().is_none() {
unresolved.push(format!("`{}.{}`", path, inline.name));
}
}
}
}
}
if !unresolved.is_empty() {
panic!(
"Failed to codegen {}/{} inline params. Could not codegen [{}]",
unresolved.len(),
params,
unresolved.join(", ")
)
}
}
}