use std::{cell::RefCell, rc::Rc};
use indexmap::IndexMap;
use newtype::Newtype;
use object::Object;
use parameter::Parameter;
use path::{Path, PrettySegments};
use proc_macro2::TokenStream;
use r#enum::Enum;
use scope::Scope;
use crate::openapi::{r#type::OpenApiType, schema::OpenApiSchema};
pub mod r#enum;
pub mod newtype;
pub mod object;
pub mod parameter;
pub mod path;
pub mod scope;
pub mod union;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Model {
Newtype(Newtype),
Enum(Enum),
Object(Object),
Unresolved,
}
impl Model {
pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
match self {
Self::Enum(r#enum) => r#enum.is_display(resolved),
Self::Newtype(_) => true,
_ => false,
}
}
}
#[derive(Default)]
pub struct ResolvedSchema {
pub models: IndexMap<String, Model>,
pub paths: IndexMap<String, Path>,
pub parameters: Vec<Parameter>,
pub warnings: WarningReporter,
}
#[derive(Clone)]
pub struct Warning {
pub message: String,
pub path: Vec<String>,
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "in {}: {}", self.path.join("."), self.message)
}
}
#[derive(Default)]
struct WarningReporterState {
warnings: Vec<Warning>,
path: Vec<String>,
}
#[derive(Clone, Default)]
pub struct WarningReporter {
state: Rc<RefCell<WarningReporterState>>,
}
impl WarningReporter {
pub fn new() -> Self {
Self::default()
}
fn push(&self, message: impl ToString) {
let mut state = self.state.borrow_mut();
let path = state.path.iter().map(ToString::to_string).collect();
state.warnings.push(Warning {
message: message.to_string(),
path,
});
}
fn child(&self, name: impl ToString) -> WarningReporter {
self.state.borrow_mut().path.push(name.to_string());
Self {
state: self.state.clone(),
}
}
pub fn is_empty(&self) -> bool {
self.state.borrow().warnings.is_empty()
}
pub fn get_warnings(&self) -> Vec<Warning> {
self.state.borrow().warnings.clone()
}
}
impl Drop for WarningReporter {
fn drop(&mut self) {
self.state.borrow_mut().path.pop();
}
}
impl ResolvedSchema {
pub fn from_open_api(schema: &OpenApiSchema) -> Self {
let mut result = Self::default();
for (name, r#type) in &schema.components.schemas {
result.models.insert(
name.to_string(),
resolve(r#type, name, &schema.components.schemas, &result.warnings),
);
}
for (path, body) in &schema.paths {
result.paths.insert(
path.to_string(),
Path::from_schema(
path,
body,
&schema.components.parameters,
result.warnings.child(path),
)
.unwrap(),
);
}
for (name, param) in &schema.components.parameters {
result.parameters.push(
Parameter::from_schema(name, param, result.warnings.child(name.to_owned()))
.unwrap(),
);
}
result
}
pub fn codegen_models(&self) -> TokenStream {
let mut output = TokenStream::default();
for model in self.models.values() {
output.extend(model.codegen(self));
}
output
}
pub fn codegen_requests(&self) -> TokenStream {
let mut output = TokenStream::default();
for path in self.paths.values() {
output.extend(
path.codegen_request(self, self.warnings.child(PrettySegments(&path.segments))),
);
}
output
}
pub fn codegen_parameters(&self) -> TokenStream {
let mut output = TokenStream::default();
for param in &self.parameters {
output.extend(param.codegen(self));
}
output
}
pub fn codegen_scopes(&self) -> TokenStream {
let mut output = TokenStream::default();
let scopes = Scope::from_paths(self.paths.values().cloned().collect());
for scope in scopes {
output.extend(scope.codegen());
}
output
}
}
pub fn resolve(
r#type: &OpenApiType,
name: &str,
schemas: &IndexMap<&str, OpenApiType>,
warnings: &WarningReporter,
) -> Model {
match r#type {
OpenApiType {
r#enum: Some(_), ..
} => Enum::from_schema(name, r#type, warnings.child(name))
.map_or(Model::Unresolved, Model::Enum),
OpenApiType {
r#type: Some("object"),
..
} => Model::Object(Object::from_schema_object(
name,
r#type,
schemas,
warnings.child(name),
)),
OpenApiType {
r#type: Some(_), ..
} => Newtype::from_schema(name, r#type).map_or(Model::Unresolved, Model::Newtype),
OpenApiType {
one_of: Some(types),
..
} => Enum::from_one_of(name, types, warnings.child(name))
.map_or(Model::Unresolved, Model::Enum),
OpenApiType {
all_of: Some(types),
..
} => Model::Object(Object::from_all_of(
name,
types,
schemas,
warnings.child(name),
)),
_ => Model::Unresolved,
}
}
impl Model {
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
match self {
Self::Newtype(newtype) => newtype.codegen(),
Self::Enum(r#enum) => r#enum.codegen(resolved),
Self::Object(object) => object.codegen(resolved),
Self::Unresolved => None,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::openapi::schema::test::get_schema;
#[test]
fn resolve_newtypes() {
let schema = get_schema();
let user_id_schema = schema.components.schemas.get("UserId").unwrap();
let reporter = WarningReporter::new();
let user_id = resolve(
user_id_schema,
"UserId",
&schema.components.schemas,
&reporter,
);
assert!(reporter.is_empty());
assert_eq!(
user_id,
Model::Newtype(Newtype {
name: "UserId".to_owned(),
description: None,
inner: newtype::NewtypeInner::I32,
copy: true,
ord: true
})
);
let attack_code_schema = schema.components.schemas.get("AttackCode").unwrap();
let attack_code = resolve(
attack_code_schema,
"AttackCode",
&schema.components.schemas,
&reporter,
);
assert!(reporter.is_empty());
assert_eq!(
attack_code,
Model::Newtype(Newtype {
name: "AttackCode".to_owned(),
description: None,
inner: newtype::NewtypeInner::Str,
copy: false,
ord: false
})
);
}
#[test]
fn resolve_all() {
let schema = get_schema();
let mut unresolved = vec![];
let total = schema.components.schemas.len();
for (name, desc) in &schema.components.schemas {
let reporter = WarningReporter::new();
if resolve(desc, name, &schema.components.schemas, &reporter) == Model::Unresolved
|| !reporter.is_empty()
{
unresolved.push(name);
}
}
if !unresolved.is_empty() {
panic!(
"Failed to resolve {}/{} types. Could not resolve [{}]",
unresolved.len(),
total,
unresolved
.into_iter()
.map(|u| format!("`{u}`"))
.collect::<Vec<_>>()
.join(", ")
)
}
}
}