feat(v2): initial commit
This commit is contained in:
parent
48868983b3
commit
5a84558d89
44 changed files with 20091 additions and 3489 deletions
2
torn-api-codegen/src/lib.rs
Normal file
2
torn-api-codegen/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod model;
|
||||
pub mod openapi;
|
||||
310
torn-api-codegen/src/model/enum.rs
Normal file
310
torn-api-codegen/src/model/enum.rs
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
use heck::ToUpperCamelCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::openapi::{
|
||||
parameter::OpenApiParameterSchema,
|
||||
r#type::{OpenApiType, OpenApiVariants},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EnumRepr {
|
||||
U8,
|
||||
U32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EnumVariantTupleValue {
|
||||
Ref(String),
|
||||
}
|
||||
|
||||
impl EnumVariantTupleValue {
|
||||
pub fn from_schema(schema: &OpenApiType) -> Option<Self> {
|
||||
if let OpenApiType {
|
||||
ref_path: Some(path),
|
||||
..
|
||||
} = schema
|
||||
{
|
||||
Some(Self::Ref((*path).to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
let Self::Ref(path) = self;
|
||||
|
||||
path.strip_prefix("#/components/schemas/")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EnumVariantValue {
|
||||
Repr(u32),
|
||||
String { rename: Option<String> },
|
||||
Tuple(Vec<EnumVariantTupleValue>),
|
||||
}
|
||||
|
||||
impl Default for EnumVariantValue {
|
||||
fn default() -> Self {
|
||||
Self::String { rename: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl EnumVariantValue {
|
||||
pub fn codegen_display(&self, name: &str) -> Option<TokenStream> {
|
||||
match self {
|
||||
Self::Repr(i) => Some(quote! { write!(f, "{}", #i) }),
|
||||
Self::String { rename } => {
|
||||
let name = rename.as_deref().unwrap_or(name);
|
||||
Some(quote! { write!(f, #name) })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct EnumVariant {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub value: EnumVariantValue,
|
||||
}
|
||||
|
||||
impl EnumVariant {
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
let doc = self.description.as_ref().map(|d| {
|
||||
quote! {
|
||||
#[doc = #d]
|
||||
}
|
||||
});
|
||||
|
||||
let name = format_ident!("{}", self.name);
|
||||
|
||||
match &self.value {
|
||||
EnumVariantValue::Repr(repr) => Some(quote! {
|
||||
#doc
|
||||
#name = #repr
|
||||
}),
|
||||
EnumVariantValue::String { rename } => {
|
||||
let serde_attr = rename.as_ref().map(|r| {
|
||||
quote! {
|
||||
#[serde(rename = #r)]
|
||||
}
|
||||
});
|
||||
|
||||
Some(quote! {
|
||||
#doc
|
||||
#serde_attr
|
||||
#name
|
||||
})
|
||||
}
|
||||
EnumVariantValue::Tuple(values) => {
|
||||
let mut val_tys = Vec::with_capacity(values.len());
|
||||
|
||||
for value in values {
|
||||
let ty_name = value.name()?;
|
||||
let ty_name = format_ident!("{ty_name}");
|
||||
|
||||
val_tys.push(quote! {
|
||||
crate::models::#ty_name
|
||||
});
|
||||
}
|
||||
|
||||
Some(quote! {
|
||||
#name(#(#val_tys),*)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen_display(&self) -> Option<TokenStream> {
|
||||
let rhs = self.value.codegen_display(&self.name)?;
|
||||
let name = format_ident!("{}", self.name);
|
||||
|
||||
Some(quote! {
|
||||
Self::#name => #rhs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Enum {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub repr: Option<EnumRepr>,
|
||||
pub copy: bool,
|
||||
pub display: bool,
|
||||
pub untagged: bool,
|
||||
pub variants: Vec<EnumVariant>,
|
||||
}
|
||||
|
||||
impl Enum {
|
||||
pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
|
||||
let mut result = Enum {
|
||||
name: name.to_owned(),
|
||||
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||
copy: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match &schema.r#enum {
|
||||
Some(OpenApiVariants::Int(int_variants)) => {
|
||||
result.repr = Some(EnumRepr::U32);
|
||||
result.display = true;
|
||||
result.variants = int_variants
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|i| EnumVariant {
|
||||
name: format!("Variant{i}"),
|
||||
value: EnumVariantValue::Repr(i as u32),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Some(OpenApiVariants::Str(str_variants)) => {
|
||||
result.display = true;
|
||||
result.variants = str_variants
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|s| {
|
||||
let transformed = s.replace('&', "And").to_upper_camel_case();
|
||||
EnumVariant {
|
||||
value: EnumVariantValue::String {
|
||||
rename: (transformed != s).then(|| s.to_owned()),
|
||||
},
|
||||
name: transformed,
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn from_parameter_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
|
||||
let mut result = Self {
|
||||
name: name.to_owned(),
|
||||
copy: true,
|
||||
display: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for var in schema.r#enum.as_ref()? {
|
||||
let transformed = var.to_upper_camel_case();
|
||||
result.variants.push(EnumVariant {
|
||||
value: EnumVariantValue::String {
|
||||
rename: (transformed != *var).then(|| transformed.clone()),
|
||||
},
|
||||
name: transformed,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn from_one_of(name: &str, schemas: &[OpenApiType]) -> Option<Self> {
|
||||
let mut result = Self {
|
||||
name: name.to_owned(),
|
||||
untagged: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for schema in schemas {
|
||||
let value = EnumVariantTupleValue::from_schema(schema)?;
|
||||
let name = value.name()?.to_owned();
|
||||
|
||||
result.variants.push(EnumVariant {
|
||||
name,
|
||||
value: EnumVariantValue::Tuple(vec![value]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
let repr = self.repr.map(|r| match r {
|
||||
EnumRepr::U8 => quote! { #[repr(u8)]},
|
||||
EnumRepr::U32 => quote! { #[repr(u32)]},
|
||||
});
|
||||
let name = format_ident!("{}", self.name);
|
||||
let desc = self.description.as_ref().map(|d| {
|
||||
quote! {
|
||||
#repr
|
||||
#[doc = #d]
|
||||
}
|
||||
});
|
||||
|
||||
let mut display = Vec::with_capacity(self.variants.len());
|
||||
let mut variants = Vec::with_capacity(self.variants.len());
|
||||
for variant in &self.variants {
|
||||
variants.push(variant.codegen()?);
|
||||
|
||||
if self.display {
|
||||
display.push(variant.codegen_display()?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut derives = vec![];
|
||||
|
||||
if self.copy {
|
||||
derives.extend_from_slice(&["Copy", "Hash"]);
|
||||
}
|
||||
|
||||
let derives = derives.into_iter().map(|d| format_ident!("{d}"));
|
||||
|
||||
let serde_attr = self.untagged.then(|| {
|
||||
quote! {
|
||||
#[serde(untagged)]
|
||||
}
|
||||
});
|
||||
|
||||
let display = self.display.then(|| {
|
||||
quote! {
|
||||
impl std::fmt::Display for #name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
#(#display),*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Some(quote! {
|
||||
#desc
|
||||
#[derive(Debug, Clone, PartialEq, serde::Deserialize, #(#derives),*)]
|
||||
#serde_attr
|
||||
pub enum #name {
|
||||
#(#variants),*
|
||||
}
|
||||
#display
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::openapi::schema::OpenApiSchema;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codegen() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let revive_setting = schema.components.schemas.get("ReviveSetting").unwrap();
|
||||
|
||||
let r#enum = Enum::from_schema("ReviveSetting", revive_setting).unwrap();
|
||||
|
||||
let code = r#enum.codegen().unwrap();
|
||||
|
||||
panic!("{code}");
|
||||
}
|
||||
}
|
||||
189
torn-api-codegen/src/model/mod.rs
Normal file
189
torn-api-codegen/src/model/mod.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
use r#enum::Enum;
|
||||
use indexmap::IndexMap;
|
||||
use newtype::Newtype;
|
||||
use object::Object;
|
||||
use proc_macro2::TokenStream;
|
||||
|
||||
use crate::openapi::r#type::OpenApiType;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
pub fn resolve(r#type: &OpenApiType, name: &str, schemas: &IndexMap<&str, OpenApiType>) -> Model {
|
||||
match r#type {
|
||||
OpenApiType {
|
||||
r#enum: Some(_), ..
|
||||
} => Enum::from_schema(name, r#type).map_or(Model::Unresolved, Model::Enum),
|
||||
OpenApiType {
|
||||
r#type: Some("object"),
|
||||
..
|
||||
} => Object::from_schema_object(name, r#type, schemas)
|
||||
.map_or(Model::Unresolved, Model::Object),
|
||||
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).map_or(Model::Unresolved, Model::Enum),
|
||||
OpenApiType {
|
||||
all_of: Some(types),
|
||||
..
|
||||
} => Object::from_all_of(name, types, schemas).map_or(Model::Unresolved, Model::Object),
|
||||
_ => Model::Unresolved,
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
match self {
|
||||
Self::Newtype(newtype) => newtype.codegen(),
|
||||
Self::Enum(r#enum) => r#enum.codegen(),
|
||||
Self::Object(object) => object.codegen(),
|
||||
Self::Unresolved => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
model::r#enum::{EnumRepr, EnumVariant},
|
||||
openapi::schema::OpenApiSchema,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn resolve_newtypes() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let user_id_schema = schema.components.schemas.get("UserId").unwrap();
|
||||
|
||||
let user_id = resolve(user_id_schema, "UserId", &schema.components.schemas);
|
||||
|
||||
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);
|
||||
|
||||
assert_eq!(
|
||||
attack_code,
|
||||
Model::Newtype(Newtype {
|
||||
name: "AttackCode".to_owned(),
|
||||
description: None,
|
||||
inner: newtype::NewtypeInner::Str,
|
||||
copy: false,
|
||||
ord: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_enums() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let forum_feed_type_schema = schema.components.schemas.get("ForumFeedTypeEnum").unwrap();
|
||||
|
||||
let forum_feed_type = resolve(
|
||||
forum_feed_type_schema,
|
||||
"ForumFeedTypeEnum",
|
||||
&schema.components.schemas,
|
||||
);
|
||||
|
||||
assert_eq!(forum_feed_type, Model::Enum(Enum {
|
||||
name: "ForumFeedType".to_owned(),
|
||||
description: Some("This represents the type of the activity. Values range from 1 to 8 where:\n * 1 = 'X posted on a thread',\n * 2 = 'X created a thread',\n * 3 = 'X liked your thread',\n * 4 = 'X disliked your thread',\n * 5 = 'X liked your post',\n * 6 = 'X disliked your post',\n * 7 = 'X quoted your post'.".to_owned()),
|
||||
repr: Some(EnumRepr::U32),
|
||||
copy: true,
|
||||
untagged: false,
|
||||
display: true,
|
||||
variants: vec![
|
||||
EnumVariant {
|
||||
name: "Variant1".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(1),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant2".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(2),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant3".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(3),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant4".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(4),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant5".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(5),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant6".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(6),
|
||||
..Default::default()
|
||||
},
|
||||
EnumVariant {
|
||||
name: "Variant7".to_owned(),
|
||||
value: r#enum::EnumVariantValue::Repr(7),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_all() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let mut unresolved = vec![];
|
||||
let total = schema.components.schemas.len();
|
||||
|
||||
for (name, desc) in &schema.components.schemas {
|
||||
if resolve(desc, name, &schema.components.schemas) == Model::Unresolved {
|
||||
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(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
144
torn-api-codegen/src/model/newtype.rs
Normal file
144
torn-api-codegen/src/model/newtype.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::openapi::r#type::OpenApiType;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NewtypeInner {
|
||||
Str,
|
||||
I32,
|
||||
I64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Newtype {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub inner: NewtypeInner,
|
||||
pub copy: bool,
|
||||
pub ord: bool,
|
||||
}
|
||||
|
||||
impl Newtype {
|
||||
pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
|
||||
let name = name.to_owned();
|
||||
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||
|
||||
match schema {
|
||||
OpenApiType {
|
||||
r#type: Some("string"),
|
||||
..
|
||||
} => Some(Self {
|
||||
name,
|
||||
description,
|
||||
inner: NewtypeInner::Str,
|
||||
copy: false,
|
||||
ord: false,
|
||||
}),
|
||||
OpenApiType {
|
||||
r#type: Some("integer"),
|
||||
format: Some("int32"),
|
||||
..
|
||||
} => Some(Self {
|
||||
name,
|
||||
description,
|
||||
inner: NewtypeInner::I32,
|
||||
copy: true,
|
||||
ord: true,
|
||||
}),
|
||||
OpenApiType {
|
||||
r#type: Some("integer"),
|
||||
format: Some("int64"),
|
||||
..
|
||||
} => Some(Self {
|
||||
name,
|
||||
description,
|
||||
inner: NewtypeInner::I64,
|
||||
copy: true,
|
||||
ord: true,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> Option<proc_macro2::TokenStream> {
|
||||
let mut derives = vec![quote! { Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize }];
|
||||
|
||||
if self.copy {
|
||||
derives.push(quote! { Copy });
|
||||
}
|
||||
|
||||
if self.ord {
|
||||
derives.push(quote! { PartialOrd, Ord });
|
||||
}
|
||||
|
||||
let name = format_ident!("{}", self.name);
|
||||
let inner = match self.inner {
|
||||
NewtypeInner::Str => format_ident!("String"),
|
||||
NewtypeInner::I32 => format_ident!("i32"),
|
||||
NewtypeInner::I64 => format_ident!("i64"),
|
||||
};
|
||||
|
||||
let doc = self.description.as_ref().map(|d| {
|
||||
quote! {
|
||||
#[doc = #d]
|
||||
}
|
||||
});
|
||||
|
||||
let body = quote! {
|
||||
#doc
|
||||
#[derive(#(#derives),*)]
|
||||
pub struct #name(pub #inner);
|
||||
|
||||
impl #name {
|
||||
pub fn new(inner: #inner) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> #inner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<#inner> for #name {
|
||||
fn from(inner: #inner) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<#name> for #inner {
|
||||
fn from(outer: #name) -> Self {
|
||||
outer.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for #name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(body)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::openapi::schema::OpenApiSchema;
|
||||
|
||||
#[test]
|
||||
fn codegen() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let user_id = schema.components.schemas.get("UserId").unwrap();
|
||||
|
||||
let mut newtype = Newtype::from_schema("UserId", user_id).unwrap();
|
||||
|
||||
newtype.description = Some("Description goes here".to_owned());
|
||||
|
||||
let code = newtype.codegen().unwrap().to_string();
|
||||
|
||||
panic!("{code}");
|
||||
}
|
||||
}
|
||||
448
torn-api-codegen/src/model/object.rs
Normal file
448
torn-api-codegen/src/model/object.rs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||
use indexmap::IndexMap;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::Ident;
|
||||
|
||||
use crate::openapi::r#type::OpenApiType;
|
||||
|
||||
use super::r#enum::Enum;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PrimitiveType {
|
||||
Bool,
|
||||
I32,
|
||||
I64,
|
||||
String,
|
||||
Float,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PropertyType {
|
||||
Primitive(PrimitiveType),
|
||||
Ref(String),
|
||||
Enum(Enum),
|
||||
Nested(Box<Object>),
|
||||
Array(Box<PropertyType>),
|
||||
}
|
||||
|
||||
impl PropertyType {
|
||||
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
||||
match self {
|
||||
Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
|
||||
Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
|
||||
Self::Primitive(PrimitiveType::I64) => Some(format_ident!("i64").into_token_stream()),
|
||||
Self::Primitive(PrimitiveType::String) => {
|
||||
Some(format_ident!("String").into_token_stream())
|
||||
}
|
||||
Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
|
||||
Self::Ref(path) => {
|
||||
let name = path.strip_prefix("#/components/schemas/")?;
|
||||
let name = format_ident!("{name}");
|
||||
|
||||
Some(quote! { crate::models::#name })
|
||||
}
|
||||
Self::Enum(r#enum) => {
|
||||
let code = r#enum.codegen()?;
|
||||
namespace.push_element(code);
|
||||
|
||||
let ns = namespace.get_ident();
|
||||
let name = format_ident!("{}", r#enum.name);
|
||||
|
||||
Some(quote! {
|
||||
#ns::#name
|
||||
})
|
||||
}
|
||||
Self::Array(array) => {
|
||||
let inner_ty = array.codegen(namespace)?;
|
||||
|
||||
Some(quote! {
|
||||
Vec<#inner_ty>
|
||||
})
|
||||
}
|
||||
Self::Nested(nested) => {
|
||||
let code = nested.codegen()?;
|
||||
namespace.push_element(code);
|
||||
|
||||
let ns = namespace.get_ident();
|
||||
let name = format_ident!("{}", nested.name);
|
||||
|
||||
Some(quote! {
|
||||
#ns::#name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Property {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub required: bool,
|
||||
pub nullable: bool,
|
||||
pub r#type: PropertyType,
|
||||
}
|
||||
|
||||
impl Property {
|
||||
pub fn from_schema(
|
||||
name: &str,
|
||||
required: bool,
|
||||
schema: &OpenApiType,
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
) -> Option<Self> {
|
||||
let name = name.to_owned();
|
||||
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||
|
||||
match schema {
|
||||
OpenApiType {
|
||||
r#enum: Some(_), ..
|
||||
} => Some(Self {
|
||||
r#type: PropertyType::Enum(Enum::from_schema(
|
||||
&name.clone().to_upper_camel_case(),
|
||||
schema,
|
||||
)?),
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
}),
|
||||
OpenApiType {
|
||||
one_of: Some(types),
|
||||
..
|
||||
} => match types.as_slice() {
|
||||
[
|
||||
left,
|
||||
OpenApiType {
|
||||
r#type: Some("null"),
|
||||
..
|
||||
},
|
||||
] => {
|
||||
let mut inner = Self::from_schema(&name, required, left, schemas)?;
|
||||
inner.nullable = true;
|
||||
Some(inner)
|
||||
}
|
||||
[
|
||||
left @ ..,
|
||||
OpenApiType {
|
||||
r#type: Some("null"),
|
||||
..
|
||||
},
|
||||
] => {
|
||||
let rest = OpenApiType {
|
||||
one_of: Some(left.to_owned()),
|
||||
..schema.clone()
|
||||
};
|
||||
let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
|
||||
inner.nullable = true;
|
||||
Some(inner)
|
||||
}
|
||||
cases => {
|
||||
let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
|
||||
Some(Self {
|
||||
name,
|
||||
description: None,
|
||||
required,
|
||||
nullable: false,
|
||||
r#type: PropertyType::Enum(r#enum),
|
||||
})
|
||||
}
|
||||
},
|
||||
OpenApiType {
|
||||
all_of: Some(types),
|
||||
..
|
||||
} => {
|
||||
let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
|
||||
Some(Self {
|
||||
name,
|
||||
description: None,
|
||||
required,
|
||||
nullable: false,
|
||||
r#type: PropertyType::Nested(Box::new(composite)),
|
||||
})
|
||||
}
|
||||
OpenApiType {
|
||||
r#type: Some("object"),
|
||||
..
|
||||
} => Some(Self {
|
||||
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
||||
&name.clone().to_upper_camel_case(),
|
||||
schema,
|
||||
schemas,
|
||||
)?)),
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
}),
|
||||
OpenApiType {
|
||||
ref_path: Some(path),
|
||||
..
|
||||
} => Some(Self {
|
||||
name,
|
||||
description,
|
||||
r#type: PropertyType::Ref((*path).to_owned()),
|
||||
required,
|
||||
nullable: false,
|
||||
}),
|
||||
OpenApiType {
|
||||
r#type: Some("array"),
|
||||
items: Some(items),
|
||||
..
|
||||
} => {
|
||||
let inner = Self::from_schema(&name, required, items, schemas)?;
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
r#type: PropertyType::Array(Box::new(inner.r#type)),
|
||||
})
|
||||
}
|
||||
OpenApiType {
|
||||
r#type: Some(_), ..
|
||||
} => {
|
||||
let prim = match (schema.r#type, schema.format) {
|
||||
(Some("integer"), Some("int32")) => PrimitiveType::I32,
|
||||
(Some("integer"), Some("int64")) => PrimitiveType::I64,
|
||||
(Some("number"), Some("float")) => PrimitiveType::Float,
|
||||
(Some("string"), None) => PrimitiveType::String,
|
||||
(Some("boolean"), None) => PrimitiveType::Bool,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
r#type: PropertyType::Primitive(prim),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
||||
let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
|
||||
|
||||
let name = &self.name;
|
||||
let (name, serde_attr) = match name.as_str() {
|
||||
"type" => (format_ident!("r#type"), None),
|
||||
name if name != name.to_snake_case() => (
|
||||
format_ident!("{}", name.to_snake_case()),
|
||||
Some(quote! { #[serde(rename = #name)]}),
|
||||
),
|
||||
_ => (format_ident!("{name}"), None),
|
||||
};
|
||||
|
||||
let ty_inner = self.r#type.codegen(namespace)?;
|
||||
|
||||
let ty = if !self.required || self.nullable {
|
||||
quote! { Option<#ty_inner> }
|
||||
} else {
|
||||
ty_inner
|
||||
};
|
||||
|
||||
Some(quote! {
|
||||
#desc
|
||||
#serde_attr
|
||||
pub #name: #ty
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Object {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub properties: Vec<Property>,
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn from_schema_object(
|
||||
name: &str,
|
||||
schema: &OpenApiType,
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
) -> Option<Self> {
|
||||
let mut result = Object {
|
||||
name: name.to_owned(),
|
||||
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Some(props) = &schema.properties else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let required = schema.required.clone().unwrap_or_default();
|
||||
|
||||
for (prop_name, prop) in props {
|
||||
// HACK: This will cause a duplicate key otherwise
|
||||
if *prop_name == "itemDetails" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: implement custom enum for this (depends on overrides being added)
|
||||
if *prop_name == "value" && name == "TornHof" {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.properties.push(Property::from_schema(
|
||||
prop_name,
|
||||
required.contains(prop_name),
|
||||
prop,
|
||||
schemas,
|
||||
)?);
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn from_all_of(
|
||||
name: &str,
|
||||
types: &[OpenApiType],
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
) -> Option<Self> {
|
||||
let mut result = Self {
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for r#type in types {
|
||||
let r#type = if let OpenApiType {
|
||||
ref_path: Some(path),
|
||||
..
|
||||
} = r#type
|
||||
{
|
||||
let name = path.strip_prefix("#/components/schemas/")?;
|
||||
schemas.get(name)?
|
||||
} else {
|
||||
r#type
|
||||
};
|
||||
let obj = Self::from_schema_object(name, r#type, schemas)?;
|
||||
|
||||
result.description = result.description.or(obj.description);
|
||||
result.properties.extend(obj.properties);
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
let doc = self.description.as_ref().map(|d| {
|
||||
quote! {
|
||||
#[doc = #d]
|
||||
}
|
||||
});
|
||||
|
||||
let mut namespace = ObjectNamespace {
|
||||
object: self,
|
||||
ident: None,
|
||||
elements: Vec::default(),
|
||||
};
|
||||
|
||||
let mut props = Vec::with_capacity(self.properties.len());
|
||||
for prop in &self.properties {
|
||||
props.push(prop.codegen(&mut namespace)?);
|
||||
}
|
||||
|
||||
let name = format_ident!("{}", self.name);
|
||||
let ns = namespace.codegen();
|
||||
|
||||
Some(quote! {
|
||||
#ns
|
||||
|
||||
#doc
|
||||
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
|
||||
pub struct #name {
|
||||
#(#props),*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjectNamespace<'o> {
|
||||
object: &'o Object,
|
||||
ident: Option<Ident>,
|
||||
elements: Vec<TokenStream>,
|
||||
}
|
||||
|
||||
impl ObjectNamespace<'_> {
|
||||
pub fn get_ident(&mut self) -> Ident {
|
||||
self.ident
|
||||
.get_or_insert_with(|| {
|
||||
let name = self.object.name.to_snake_case();
|
||||
format_ident!("{name}")
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn push_element(&mut self, el: TokenStream) {
|
||||
self.elements.push(el);
|
||||
}
|
||||
|
||||
pub fn codegen(mut self) -> Option<TokenStream> {
|
||||
if self.elements.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let ident = self.get_ident();
|
||||
let elements = self.elements;
|
||||
Some(quote! {
|
||||
pub mod #ident {
|
||||
#(#elements)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crate::openapi::schema::OpenApiSchema;
|
||||
|
||||
#[test]
|
||||
fn resolve_object() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
|
||||
|
||||
let resolved =
|
||||
Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
|
||||
.unwrap();
|
||||
let _code = resolved.codegen().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_objects() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let mut objects = 0;
|
||||
let mut unresolved = vec![];
|
||||
|
||||
for (name, desc) in &schema.components.schemas {
|
||||
if desc.r#type == Some("object") {
|
||||
objects += 1;
|
||||
if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
|
||||
unresolved.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !unresolved.is_empty() {
|
||||
panic!(
|
||||
"Failed to resolve {}/{} objects. Could not resolve [{}]",
|
||||
unresolved.len(),
|
||||
objects,
|
||||
unresolved
|
||||
.into_iter()
|
||||
.map(|u| format!("`{u}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
431
torn-api-codegen/src/model/parameter.rs
Normal file
431
torn-api-codegen/src/model/parameter.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use heck::ToUpperCamelCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
482
torn-api-codegen/src/model/path.rs
Normal file
482
torn-api-codegen/src/model/path.rs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
use std::{fmt::Write, ops::Deref};
|
||||
|
||||
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||
use indexmap::IndexMap;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::Ident;
|
||||
|
||||
use crate::openapi::{
|
||||
parameter::OpenApiParameter,
|
||||
path::{OpenApiPath, OpenApiPathParameter, OpenApiResponseBody},
|
||||
};
|
||||
|
||||
use super::{
|
||||
parameter::{Parameter, ParameterLocation, ParameterType},
|
||||
union::Union,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PathSegment {
|
||||
Constant(String),
|
||||
Parameter { name: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PathParameter {
|
||||
Inline(Parameter),
|
||||
Component(Parameter),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PathResponse {
|
||||
Component { name: String },
|
||||
// TODO: needs to be implemented
|
||||
ArbitraryUnion(Union),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Path {
|
||||
pub segments: Vec<PathSegment>,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub description: String,
|
||||
pub parameters: Vec<PathParameter>,
|
||||
pub response: PathResponse,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn from_schema(
|
||||
path: &str,
|
||||
schema: &OpenApiPath,
|
||||
parameters: &IndexMap<&str, OpenApiParameter>,
|
||||
) -> Option<Self> {
|
||||
let mut segments = Vec::new();
|
||||
for segment in path.strip_prefix('/')?.split('/') {
|
||||
if segment.starts_with('{') && segment.ends_with('}') {
|
||||
segments.push(PathSegment::Parameter {
|
||||
name: segment[1..(segment.len() - 1)].to_owned(),
|
||||
});
|
||||
} else {
|
||||
segments.push(PathSegment::Constant(segment.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
|
||||
let description = schema.get.description.deref().to_owned();
|
||||
|
||||
let mut params = Vec::with_capacity(schema.get.parameters.len());
|
||||
for parameter in &schema.get.parameters {
|
||||
match ¶meter {
|
||||
OpenApiPathParameter::Link { ref_path } => {
|
||||
let name = ref_path
|
||||
.strip_prefix("#/components/parameters/")?
|
||||
.to_owned();
|
||||
let param = parameters.get(&name.as_str())?;
|
||||
params.push(PathParameter::Component(Parameter::from_schema(
|
||||
&name, param,
|
||||
)?));
|
||||
}
|
||||
OpenApiPathParameter::Inline(schema) => {
|
||||
let name = schema.name.to_upper_camel_case();
|
||||
let parameter = Parameter::from_schema(&name, schema)?;
|
||||
params.push(PathParameter::Inline(parameter));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut suffixes = vec![];
|
||||
let mut name = String::new();
|
||||
|
||||
for seg in &segments {
|
||||
match seg {
|
||||
PathSegment::Constant(val) => {
|
||||
name.push_str(&val.to_upper_camel_case());
|
||||
}
|
||||
PathSegment::Parameter { name } => {
|
||||
suffixes.push(format!("For{}", name.to_upper_camel_case()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for suffix in suffixes {
|
||||
name.push_str(&suffix);
|
||||
}
|
||||
|
||||
let response = match &schema.get.response_content {
|
||||
OpenApiResponseBody::Schema(link) => PathResponse::Component {
|
||||
name: link
|
||||
.ref_path
|
||||
.strip_prefix("#/components/schemas/")?
|
||||
.to_owned(),
|
||||
},
|
||||
OpenApiResponseBody::Union { any_of: _ } => PathResponse::ArbitraryUnion(
|
||||
Union::from_schema("Response", &schema.get.response_content)?,
|
||||
),
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
segments,
|
||||
name,
|
||||
summary,
|
||||
description,
|
||||
parameters: params,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn codegen_request(&self) -> Option<TokenStream> {
|
||||
let name = if self.segments.len() == 1 {
|
||||
let Some(PathSegment::Constant(first)) = self.segments.first() else {
|
||||
return None;
|
||||
};
|
||||
format_ident!("{}Request", first.to_upper_camel_case())
|
||||
} else {
|
||||
format_ident!("{}Request", self.name)
|
||||
};
|
||||
|
||||
let mut ns = PathNamespace {
|
||||
path: self,
|
||||
ident: None,
|
||||
elements: Vec::new(),
|
||||
};
|
||||
|
||||
let mut fields = Vec::with_capacity(self.parameters.len());
|
||||
let mut convert_field = Vec::with_capacity(self.parameters.len());
|
||||
let mut start_fields = Vec::new();
|
||||
let mut discriminant = Vec::new();
|
||||
let mut discriminant_val = Vec::new();
|
||||
let mut fmt_val = Vec::new();
|
||||
|
||||
for param in &self.parameters {
|
||||
let (is_inline, param) = match ¶m {
|
||||
PathParameter::Inline(param) => (true, param),
|
||||
PathParameter::Component(param) => (false, param),
|
||||
};
|
||||
|
||||
let ty = match ¶m.r#type {
|
||||
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||
let ty_name = format_ident!("{}", param.name);
|
||||
|
||||
if is_inline {
|
||||
ns.push_element(param.codegen()?);
|
||||
let path = ns.get_ident();
|
||||
|
||||
quote! {
|
||||
crate::request::models::#path::#ty_name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
crate::parameters::#ty_name
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType::String => quote! { String },
|
||||
ParameterType::Boolean => quote! { bool },
|
||||
ParameterType::Schema { type_name } => {
|
||||
let ty_name = format_ident!("{}", type_name);
|
||||
|
||||
quote! {
|
||||
crate::models::#ty_name
|
||||
}
|
||||
}
|
||||
ParameterType::Array { .. } => {
|
||||
ns.push_element(param.codegen()?);
|
||||
let ty_name = param.r#type.codegen_type_name(¶m.name);
|
||||
let path = ns.get_ident();
|
||||
quote! {
|
||||
crate::request::models::#path::#ty_name
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let name = format_ident!("{}", param.name.to_snake_case());
|
||||
let query_val = ¶m.value;
|
||||
|
||||
if param.location == ParameterLocation::Path {
|
||||
discriminant.push(ty.clone());
|
||||
discriminant_val.push(quote! { self.#name });
|
||||
let path_name = format_ident!("{}", param.value);
|
||||
start_fields.push(quote! {
|
||||
#[builder(start_fn)]
|
||||
pub #name: #ty
|
||||
});
|
||||
fmt_val.push(quote! {
|
||||
#path_name=self.#name
|
||||
});
|
||||
} else {
|
||||
let ty = if param.required {
|
||||
convert_field.push(quote! {
|
||||
.chain(std::iter::once(&self.#name).map(|v| (#query_val, v.to_string())))
|
||||
});
|
||||
ty
|
||||
} else {
|
||||
convert_field.push(quote! {
|
||||
.chain(self.#name.as_ref().into_iter().map(|v| (#query_val, v.to_string())))
|
||||
});
|
||||
quote! { Option<#ty>}
|
||||
};
|
||||
|
||||
fields.push(quote! {
|
||||
pub #name: #ty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let response_ty = match &self.response {
|
||||
PathResponse::Component { name } => {
|
||||
let name = format_ident!("{name}");
|
||||
quote! {
|
||||
crate::models::#name
|
||||
}
|
||||
}
|
||||
PathResponse::ArbitraryUnion(union) => {
|
||||
let path = ns.get_ident();
|
||||
let ty_name = format_ident!("{}", union.name);
|
||||
|
||||
quote! {
|
||||
crate::request::models::#path::#ty_name
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut path_fmt_str = String::new();
|
||||
for seg in &self.segments {
|
||||
match seg {
|
||||
PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{}", val),
|
||||
PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{}}}", name),
|
||||
}
|
||||
}
|
||||
|
||||
if let PathResponse::ArbitraryUnion(union) = &self.response {
|
||||
ns.push_element(union.codegen()?);
|
||||
}
|
||||
|
||||
let ns = ns.codegen();
|
||||
|
||||
start_fields.extend(fields);
|
||||
|
||||
Some(quote! {
|
||||
#ns
|
||||
|
||||
#[derive(Debug, Clone, bon::Builder)]
|
||||
#[builder(state_mod(vis = "pub(crate)"))]
|
||||
pub struct #name {
|
||||
#(#start_fields),*
|
||||
}
|
||||
|
||||
impl crate::request::IntoRequest for #name {
|
||||
#[allow(unused_parens)]
|
||||
type Discriminant = (#(#discriminant),*);
|
||||
type Response = #response_ty;
|
||||
fn into_request(self) -> crate::request::ApiRequest<Self::Discriminant> {
|
||||
#[allow(unused_parens)]
|
||||
crate::request::ApiRequest {
|
||||
path: format!(#path_fmt_str, #(#fmt_val),*),
|
||||
parameters: std::iter::empty()
|
||||
#(#convert_field)*
|
||||
.collect(),
|
||||
disriminant: (#(#discriminant_val),*),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn codegen_scope_call(&self) -> Option<TokenStream> {
|
||||
let mut extra_args = Vec::new();
|
||||
let mut disc = Vec::new();
|
||||
|
||||
let snake_name = self.name.to_snake_case();
|
||||
|
||||
let request_name = format_ident!("{}Request", self.name);
|
||||
let builder_name = format_ident!("{}RequestBuilder", self.name);
|
||||
let builder_mod_name = format_ident!("{}_request_builder", snake_name);
|
||||
let request_mod_name = format_ident!("{snake_name}");
|
||||
|
||||
let request_path = quote! { crate::request::models::#request_name };
|
||||
let builder_path = quote! { crate::request::models::#builder_name };
|
||||
let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
|
||||
|
||||
let tail = snake_name
|
||||
.split_once('_')
|
||||
.map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
|
||||
|
||||
let fn_name = format_ident!("{tail}");
|
||||
|
||||
for param in &self.parameters {
|
||||
let (param, is_inline) = match param {
|
||||
PathParameter::Inline(param) => (param, true),
|
||||
PathParameter::Component(param) => (param, false),
|
||||
};
|
||||
|
||||
if param.location == ParameterLocation::Path {
|
||||
let ty = match ¶m.r#type {
|
||||
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||
let ty_name = format_ident!("{}", param.name);
|
||||
|
||||
if is_inline {
|
||||
quote! {
|
||||
crate::request::models::#request_mod_name::#ty_name
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
crate::parameters::#ty_name
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType::String => quote! { String },
|
||||
ParameterType::Boolean => quote! { bool },
|
||||
ParameterType::Schema { type_name } => {
|
||||
let ty_name = format_ident!("{}", type_name);
|
||||
|
||||
quote! {
|
||||
crate::models::#ty_name
|
||||
}
|
||||
}
|
||||
ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
|
||||
};
|
||||
|
||||
let arg_name = format_ident!("{}", param.value.to_snake_case());
|
||||
|
||||
extra_args.push(quote! { #arg_name: #ty, });
|
||||
disc.push(arg_name);
|
||||
}
|
||||
}
|
||||
|
||||
let response_ty = match &self.response {
|
||||
PathResponse::Component { name } => {
|
||||
let name = format_ident!("{name}");
|
||||
quote! {
|
||||
crate::models::#name
|
||||
}
|
||||
}
|
||||
PathResponse::ArbitraryUnion(union) => {
|
||||
let name = format_ident!("{}", union.name);
|
||||
quote! {
|
||||
crate::request::models::#request_mod_name::#name
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(quote! {
|
||||
pub async fn #fn_name<S>(
|
||||
&self,
|
||||
#(#extra_args)*
|
||||
builder: impl FnOnce(
|
||||
#builder_path<#builder_mod_path::Empty>
|
||||
) -> #builder_path<S>,
|
||||
) -> Result<#response_ty, E::Error>
|
||||
where
|
||||
S: #builder_mod_path::IsComplete,
|
||||
{
|
||||
let r = builder(#request_path::builder(#(#disc),*)).build();
|
||||
|
||||
self.0.fetch(r).await
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathNamespace<'r> {
|
||||
path: &'r Path,
|
||||
ident: Option<Ident>,
|
||||
elements: Vec<TokenStream>,
|
||||
}
|
||||
|
||||
impl PathNamespace<'_> {
|
||||
pub fn get_ident(&mut self) -> Ident {
|
||||
self.ident
|
||||
.get_or_insert_with(|| {
|
||||
let name = self.path.name.to_snake_case();
|
||||
format_ident!("{name}")
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn push_element(&mut self, el: TokenStream) {
|
||||
self.elements.push(el);
|
||||
}
|
||||
|
||||
pub fn codegen(mut self) -> Option<TokenStream> {
|
||||
if self.elements.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let ident = self.get_ident();
|
||||
let elements = self.elements;
|
||||
Some(quote! {
|
||||
pub mod #ident {
|
||||
#(#elements)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crate::openapi::schema::OpenApiSchema;
|
||||
|
||||
#[test]
|
||||
fn resolve_paths() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let mut paths = 0;
|
||||
let mut unresolved = vec![];
|
||||
|
||||
for (name, desc) in &schema.paths {
|
||||
paths += 1;
|
||||
if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
|
||||
unresolved.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if !unresolved.is_empty() {
|
||||
panic!(
|
||||
"Failed to resolve {}/{} paths. Could not resolve [{}]",
|
||||
unresolved.len(),
|
||||
paths,
|
||||
unresolved
|
||||
.into_iter()
|
||||
.map(|u| format!("`{u}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codegen_paths() {
|
||||
let schema = OpenApiSchema::read().unwrap();
|
||||
|
||||
let mut paths = 0;
|
||||
let mut unresolved = vec![];
|
||||
|
||||
for (name, desc) in &schema.paths {
|
||||
paths += 1;
|
||||
let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
|
||||
unresolved.push(name);
|
||||
continue;
|
||||
};
|
||||
|
||||
if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
|
||||
unresolved.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if !unresolved.is_empty() {
|
||||
panic!(
|
||||
"Failed to codegen {}/{} paths. Could not resolve [{}]",
|
||||
unresolved.len(),
|
||||
paths,
|
||||
unresolved
|
||||
.into_iter()
|
||||
.map(|u| format!("`{u}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
torn-api-codegen/src/model/scope.rs
Normal file
64
torn-api-codegen/src/model/scope.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use heck::ToUpperCamelCase;
|
||||
use indexmap::IndexMap;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use super::path::{Path, PathSegment};
|
||||
|
||||
pub struct Scope {
|
||||
pub name: String,
|
||||
pub mod_name: String,
|
||||
pub members: Vec<Path>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn from_paths(paths: Vec<Path>) -> Vec<Scope> {
|
||||
let mut map = IndexMap::new();
|
||||
|
||||
for path in paths {
|
||||
let Some(PathSegment::Constant(first_seg)) = path.segments.first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
map.entry(first_seg.to_owned())
|
||||
.or_insert_with(|| Scope {
|
||||
name: format!("{}Scope", first_seg.to_upper_camel_case()),
|
||||
mod_name: first_seg.clone(),
|
||||
members: Vec::new(),
|
||||
})
|
||||
.members
|
||||
.push(path);
|
||||
}
|
||||
|
||||
map.into_values().collect()
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
let name = format_ident!("{}", self.name);
|
||||
|
||||
let mut functions = Vec::with_capacity(self.members.len());
|
||||
|
||||
for member in &self.members {
|
||||
if let Some(code) = member.codegen_scope_call() {
|
||||
functions.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
Some(quote! {
|
||||
pub struct #name<'e, E>(&'e E)
|
||||
where
|
||||
E: crate::executor::Executor;
|
||||
|
||||
impl<'e, E> #name<'e, E>
|
||||
where
|
||||
E: crate::executor::Executor
|
||||
{
|
||||
pub fn new(executor: &'e E) -> Self {
|
||||
Self(executor)
|
||||
}
|
||||
|
||||
#(#functions)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
torn-api-codegen/src/model/union.rs
Normal file
50
torn-api-codegen/src/model/union.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use heck::ToSnakeCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::openapi::path::OpenApiResponseBody;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Union {
|
||||
pub name: String,
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
impl Union {
|
||||
pub fn from_schema(name: &str, schema: &OpenApiResponseBody) -> Option<Self> {
|
||||
let members = match schema {
|
||||
OpenApiResponseBody::Union { any_of } => {
|
||||
any_of.iter().map(|l| l.ref_path.to_owned()).collect()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let name = name.to_owned();
|
||||
|
||||
Some(Self { name, members })
|
||||
}
|
||||
|
||||
pub fn codegen(&self) -> Option<TokenStream> {
|
||||
let name = format_ident!("{}", self.name);
|
||||
let mut variants = Vec::new();
|
||||
|
||||
for member in &self.members {
|
||||
let variant_name = member.strip_prefix("#/components/schemas/")?;
|
||||
let accessor_name = format_ident!("{}", variant_name.to_snake_case());
|
||||
let ty_name = format_ident!("{}", variant_name);
|
||||
variants.push(quote! {
|
||||
pub fn #accessor_name(&self) -> Result<crate::models::#ty_name, serde_json::Error> {
|
||||
<crate::models::#ty_name as serde::Deserialize>::deserialize(&self.0)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some(quote! {
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct #name(serde_json::Value);
|
||||
|
||||
impl #name {
|
||||
#(#variants)*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
4
torn-api-codegen/src/openapi/mod.rs
Normal file
4
torn-api-codegen/src/openapi/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod parameter;
|
||||
pub mod path;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
40
torn-api-codegen/src/openapi/parameter.rs
Normal file
40
torn-api-codegen/src/openapi/parameter.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ParameterLocation {
|
||||
Query,
|
||||
Path,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenApiParameterDefault<'a> {
|
||||
Int(i32),
|
||||
Str(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenApiParameterSchema<'a> {
|
||||
#[serde(rename = "$ref")]
|
||||
pub ref_path: Option<&'a str>,
|
||||
pub r#type: Option<&'a str>,
|
||||
pub r#enum: Option<Vec<&'a str>>,
|
||||
pub format: Option<&'a str>,
|
||||
pub default: Option<OpenApiParameterDefault<'a>>,
|
||||
pub maximum: Option<i32>,
|
||||
pub minimum: Option<i32>,
|
||||
pub items: Option<Box<OpenApiParameterSchema<'a>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenApiParameter<'a> {
|
||||
pub name: &'a str,
|
||||
pub description: Option<Cow<'a, str>>,
|
||||
pub r#in: ParameterLocation,
|
||||
pub required: bool,
|
||||
#[serde(borrow)]
|
||||
pub schema: OpenApiParameterSchema<'a>,
|
||||
}
|
||||
81
torn-api-codegen/src/openapi/path.rs
Normal file
81
torn-api-codegen/src/openapi/path.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::parameter::OpenApiParameter;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenApiPathParameter<'a> {
|
||||
Link {
|
||||
#[serde(rename = "$ref")]
|
||||
ref_path: &'a str,
|
||||
},
|
||||
Inline(OpenApiParameter<'a>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SchemaLink<'a> {
|
||||
#[serde(rename = "$ref")]
|
||||
pub ref_path: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenApiResponseBody<'a> {
|
||||
Schema(SchemaLink<'a>),
|
||||
Union {
|
||||
#[serde(borrow, rename = "anyOf")]
|
||||
any_of: Vec<SchemaLink<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
fn deserialize_response_body<'de, D>(deserializer: D) -> Result<OpenApiResponseBody<'de>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Json<'a> {
|
||||
#[serde(borrow)]
|
||||
schema: OpenApiResponseBody<'a>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Content<'a> {
|
||||
#[serde(borrow, rename = "application/json")]
|
||||
json: Json<'a>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct StatusOk<'a> {
|
||||
#[serde(borrow)]
|
||||
content: Content<'a>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Responses<'a> {
|
||||
#[serde(borrow, rename = "200")]
|
||||
ok: StatusOk<'a>,
|
||||
}
|
||||
|
||||
let responses = Responses::deserialize(deserializer)?;
|
||||
|
||||
Ok(responses.ok.content.json.schema)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenApiPathBody<'a> {
|
||||
pub summary: Option<Cow<'a, str>>,
|
||||
pub description: Cow<'a, str>,
|
||||
#[serde(borrow, default)]
|
||||
pub parameters: Vec<OpenApiPathParameter<'a>>,
|
||||
#[serde(
|
||||
borrow,
|
||||
rename = "responses",
|
||||
deserialize_with = "deserialize_response_body"
|
||||
)]
|
||||
pub response_content: OpenApiResponseBody<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenApiPath<'a> {
|
||||
#[serde(borrow)]
|
||||
pub get: OpenApiPathBody<'a>,
|
||||
}
|
||||
38
torn-api-codegen/src/openapi/schema.rs
Normal file
38
torn-api-codegen/src/openapi/schema.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{parameter::OpenApiParameter, path::OpenApiPath, r#type::OpenApiType};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Components<'a> {
|
||||
#[serde(borrow)]
|
||||
pub schemas: IndexMap<&'a str, OpenApiType<'a>>,
|
||||
#[serde(borrow)]
|
||||
pub parameters: IndexMap<&'a str, OpenApiParameter<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenApiSchema<'a> {
|
||||
#[serde(borrow)]
|
||||
pub paths: IndexMap<&'a str, OpenApiPath<'a>>,
|
||||
#[serde(borrow)]
|
||||
pub components: Components<'a>,
|
||||
}
|
||||
|
||||
impl OpenApiSchema<'_> {
|
||||
pub fn read() -> Result<Self, serde_json::Error> {
|
||||
let s = include_str!("../../openapi.json");
|
||||
|
||||
serde_json::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
OpenApiSchema::read().unwrap();
|
||||
}
|
||||
}
|
||||
98
torn-api-codegen/src/openapi/type.rs
Normal file
98
torn-api-codegen/src/openapi/type.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenApiVariants<'a> {
|
||||
Int(Vec<i32>),
|
||||
#[serde(borrow)]
|
||||
Str(Vec<&'a str>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenApiType<'a> {
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
pub description: Option<Cow<'a, str>>,
|
||||
|
||||
pub r#type: Option<&'a str>,
|
||||
pub format: Option<&'a str>,
|
||||
|
||||
#[serde(rename = "$ref")]
|
||||
pub ref_path: Option<&'a str>,
|
||||
|
||||
pub one_of: Option<Vec<OpenApiType<'a>>>,
|
||||
pub all_of: Option<Vec<OpenApiType<'a>>>,
|
||||
|
||||
pub required: Option<Vec<&'a str>>,
|
||||
#[serde(borrow)]
|
||||
pub properties: Option<IndexMap<&'a str, OpenApiType<'a>>>,
|
||||
|
||||
pub items: Option<Box<OpenApiType<'a>>>,
|
||||
pub r#enum: Option<OpenApiVariants<'a>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn object() {
|
||||
let json = r##"
|
||||
{
|
||||
"required": [
|
||||
"name",
|
||||
"branches"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"branches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TornFactionTreeBranch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
"##;
|
||||
|
||||
let obj: OpenApiType = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(obj.r#type, Some("object"));
|
||||
|
||||
let props = obj.properties.unwrap();
|
||||
|
||||
assert!(props.contains_key("name"));
|
||||
|
||||
let branches = props.get("branches").unwrap();
|
||||
assert_eq!(branches.r#type, Some("array"));
|
||||
|
||||
let items = branches.items.as_ref().unwrap();
|
||||
assert!(items.ref_path.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_variants() {
|
||||
let int_json = r#"
|
||||
[1, 2, 3, 4]
|
||||
"#;
|
||||
|
||||
let de: OpenApiVariants = serde_json::from_str(int_json).unwrap();
|
||||
|
||||
assert_eq!(de, OpenApiVariants::Int(vec![1, 2, 3, 4]));
|
||||
|
||||
let str_json = r#"
|
||||
["foo", "bar", "baz"]
|
||||
"#;
|
||||
|
||||
let de: OpenApiVariants = serde_json::from_str(str_json).unwrap();
|
||||
|
||||
assert_eq!(de, OpenApiVariants::Str(vec!["foo", "bar", "baz"]));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue