feat(v2): initial commit

This commit is contained in:
TotallyNot 2025-04-24 13:32:02 +02:00
parent 48868983b3
commit 5a84558d89
44 changed files with 20091 additions and 3489 deletions

View file

@ -0,0 +1,2 @@
pub mod model;
pub mod openapi;

View 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}");
}
}

View 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(", ")
)
}
}
}

View 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}");
}
}

View 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(", ")
)
}
}
}

View 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(", ")
)
}
}
}

View 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 &parameter {
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 &param {
PathParameter::Inline(param) => (true, param),
PathParameter::Component(param) => (false, param),
};
let ty = match &param.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(&param.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 = &param.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 &param.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(&param.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(", ")
)
}
}
}

View 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)*
}
})
}
}

View 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)*
}
})
}
}

View file

@ -0,0 +1,4 @@
pub mod parameter;
pub mod path;
pub mod schema;
pub mod r#type;

View 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>,
}

View 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>,
}

View 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();
}
}

View 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"]));
}
}