323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
use heck::*;
|
|
use indexmap::IndexMap;
|
|
|
|
const KEYWORDS: &[&str] = &[
|
|
"type", "match", "enum", "struct", "fn", "mod", "pub", "use", "crate", "self", "super", "as",
|
|
"in", "let", "mut", "ref", "static", "trait", "where", "box",
|
|
];
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct JellyfinOpenapi {
|
|
components: Components,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct Components {
|
|
schemas: indexmap::IndexMap<String, Schema>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct Schema {
|
|
#[serde(rename = "type")]
|
|
_type: Types,
|
|
properties: Option<indexmap::IndexMap<String, Property>>,
|
|
#[serde(rename = "oneOf")]
|
|
one_of: Option<Vec<RefName>>,
|
|
#[serde(rename = "enum")]
|
|
_enum: Option<Vec<String>>,
|
|
description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct RefName {
|
|
#[serde(rename = "$ref")]
|
|
_ref: String,
|
|
}
|
|
|
|
impl RefName {
|
|
pub fn get_type_name(&self) -> String {
|
|
self._ref.split('/').last().unwrap().to_string()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct Property {
|
|
#[serde(rename = "type")]
|
|
_type: Option<Types>,
|
|
nullable: Option<bool>,
|
|
format: Option<String>,
|
|
items: Option<Box<Property>>,
|
|
properties: Option<indexmap::IndexMap<String, Property>>,
|
|
#[serde(rename = "additionalProperties")]
|
|
additional_properties: Option<Box<Property>>,
|
|
#[serde(rename = "enum")]
|
|
_enum: Option<Vec<String>>,
|
|
#[serde(rename = "allOf")]
|
|
all_of: Option<Vec<RefName>>,
|
|
#[serde(rename = "oneOf")]
|
|
one_of: Option<Vec<RefName>>,
|
|
description: Option<String>,
|
|
#[serde(rename = "$ref")]
|
|
_ref: Option<String>,
|
|
}
|
|
|
|
impl Property {
|
|
pub fn to_string(&self) -> String {
|
|
let out = match self._type {
|
|
Some(Types::String) => match self.format.as_deref() {
|
|
Some("uuid") => "uuid::Uuid".to_string(),
|
|
Some("date-time") => "jiff::Timestamp".to_string(),
|
|
_ => "String".to_string(),
|
|
},
|
|
Some(Types::Integer) => match self.format.as_deref() {
|
|
Some("int32") => "i32".to_string(),
|
|
Some("int64") => "i64".to_string(),
|
|
_ => "i32".to_string(),
|
|
},
|
|
Some(Types::Boolean) => "bool".to_string(),
|
|
Some(Types::Number) => match self.format.as_deref() {
|
|
Some("double") => "f64".to_string(),
|
|
Some("float") => "f32".to_string(),
|
|
_ => "f64".to_string(),
|
|
},
|
|
Some(Types::Array) => {
|
|
if let Some(ref items) = self.items {
|
|
format!("Vec<{}>", items.to_string())
|
|
} else {
|
|
"Vec<()>".to_string()
|
|
}
|
|
}
|
|
Some(Types::Object) => {
|
|
if let Some(properties) = &self.additional_properties {
|
|
format!(
|
|
"std::collections::HashMap<String, {}>",
|
|
properties.to_string()
|
|
)
|
|
// } else if let Some(props) = &self.properties {
|
|
// todo!()
|
|
} else {
|
|
"std::collections::HashMap<String, serde_json::Value>".to_string()
|
|
}
|
|
}
|
|
|
|
None => {
|
|
if let Some(ref _ref) = self._ref {
|
|
_ref.split('/').last().unwrap().to_string()
|
|
} else if let Some(ref all_of) = self.all_of {
|
|
all_of[0].get_type_name()
|
|
} else {
|
|
"()".into()
|
|
}
|
|
}
|
|
};
|
|
if let Some(true) = self.nullable {
|
|
format!("Option<{}>", out)
|
|
} else if self.nullable.is_none() && self._type == Some(Types::Object) {
|
|
format!("Option<{}>", out)
|
|
} else {
|
|
out
|
|
}
|
|
}
|
|
pub fn description(&self) -> Option<String> {
|
|
self.description.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Types {
|
|
Object,
|
|
String,
|
|
Boolean,
|
|
Array,
|
|
Integer,
|
|
Number,
|
|
}
|
|
|
|
fn main() {
|
|
let json = include_str!("../../jellyfin.json");
|
|
let jellyfin_openapi: JellyfinOpenapi = serde_json::from_str(json).unwrap();
|
|
let structs: IndexMap<String, Schema> = jellyfin_openapi
|
|
.components
|
|
.schemas
|
|
.iter()
|
|
.filter(|(_k, v)| v.properties.is_some())
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
let enums: IndexMap<String, Schema> = jellyfin_openapi
|
|
.components
|
|
.schemas
|
|
.iter()
|
|
.filter(|(_, v)| v._enum.is_some())
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
|
|
let syn_structs: Vec<syn::Item> = structs
|
|
.iter()
|
|
.map(|(key, value)| generate_struct(key, value))
|
|
.flatten()
|
|
.collect();
|
|
|
|
let syn_enums = enums
|
|
.iter()
|
|
.map(|(key, value)| {
|
|
let variants = value
|
|
._enum
|
|
.as_ref()
|
|
.expect("Possible oneOf")
|
|
.iter()
|
|
.map(|variant| {
|
|
let og_variant = variant.clone();
|
|
let name =
|
|
syn::Ident::new(&variant.to_pascal_case(), proc_macro2::Span::call_site());
|
|
syn::parse_quote! {
|
|
#[serde(rename = #og_variant)]
|
|
#name
|
|
}
|
|
})
|
|
.collect::<Vec<syn::Variant>>();
|
|
let key = modify_keyword(key);
|
|
let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
|
|
let desc = value.description.clone();
|
|
let tokens = if let Some(desc) = desc {
|
|
let desc = format!(" {}", desc);
|
|
quote::quote! {
|
|
#[doc = #desc]
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub enum #key {
|
|
#(#variants),*
|
|
}
|
|
}
|
|
} else {
|
|
quote::quote! {
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub enum #key {
|
|
#(#variants),*
|
|
}
|
|
}
|
|
};
|
|
syn::parse2(tokens).expect("Failed to parse enum")
|
|
})
|
|
.collect::<Vec<syn::ItemEnum>>();
|
|
|
|
let file = syn::File {
|
|
shebang: None,
|
|
attrs: vec![],
|
|
items: syn_structs
|
|
.into_iter()
|
|
.chain(syn_enums.into_iter().map(syn::Item::Enum))
|
|
.collect(),
|
|
};
|
|
let code = prettyplease::unparse(&file);
|
|
std::fs::write("jellyfin.rs", code).expect("Unable to write file");
|
|
}
|
|
|
|
fn modify_keyword(name: &str) -> String {
|
|
if KEYWORDS.contains(&name) {
|
|
format!("_{}", name)
|
|
} else {
|
|
name.to_string()
|
|
}
|
|
}
|
|
|
|
fn generate_struct(key: impl AsRef<str>, value: &Schema) -> Vec<syn::Item> {
|
|
let key = key.as_ref();
|
|
|
|
let extra_structs = value
|
|
.properties
|
|
.as_ref()
|
|
.expect("Possible properties")
|
|
.iter()
|
|
.filter_map(|(name, property)| {
|
|
if property._type == Some(Types::Object) && property.properties.is_some() {
|
|
Some(generate_struct(
|
|
&format!("{}_{}", key, name),
|
|
&Schema {
|
|
_type: Types::Object,
|
|
properties: property.properties.clone(),
|
|
one_of: None,
|
|
_enum: None,
|
|
description: property.description.clone(),
|
|
},
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.flatten()
|
|
.collect::<Vec<syn::Item>>();
|
|
|
|
let fields = value
|
|
.properties
|
|
.as_ref()
|
|
.expect("Possible properties")
|
|
.iter()
|
|
.map(|(name, property)| {
|
|
let nested_struct =
|
|
property._type == Some(Types::Object) && property.properties.is_some();
|
|
let og_name = name.clone();
|
|
let name = modify_keyword(&name.to_snake_case());
|
|
let _type_desc = property.description();
|
|
let _type_desc = if let Some(desc) = &_type_desc {
|
|
Some(format!(" {}", desc))
|
|
} else {
|
|
None
|
|
};
|
|
let _type = if !nested_struct {
|
|
property.to_string()
|
|
} else {
|
|
format!("{}_{}", key, og_name).to_pascal_case()
|
|
};
|
|
let _type = if _type.contains(key) {
|
|
_type.replace(&format!("<{}>", key), format!("<Box<{}>>", key).as_str())
|
|
} else {
|
|
_type
|
|
};
|
|
syn::Field {
|
|
attrs: if let Some(desc) = _type_desc {
|
|
syn::parse_quote! {
|
|
#[doc = #desc]
|
|
#[serde(rename = #og_name)]
|
|
}
|
|
} else {
|
|
syn::parse_quote! {
|
|
#[serde(rename = #og_name)]
|
|
}
|
|
},
|
|
mutability: syn::FieldMutability::None,
|
|
vis: syn::Visibility::Public(syn::Token)),
|
|
ident: Some(syn::Ident::new(&name, proc_macro2::Span::call_site())),
|
|
colon_token: Some(syn::token::Colon(proc_macro2::Span::call_site())),
|
|
ty: syn::parse_str(&_type).expect("Failed to parse type"),
|
|
}
|
|
})
|
|
.collect::<Vec<syn::Field>>();
|
|
|
|
let key = modify_keyword(key);
|
|
let desc = value.description.clone();
|
|
let key = syn::Ident::new(&key.to_pascal_case(), proc_macro2::Span::call_site());
|
|
let tokens = if let Some(desc) = desc {
|
|
let desc = format!(" {}", desc);
|
|
quote::quote! {
|
|
#[doc = #desc]
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct #key {
|
|
#(#fields),*
|
|
}
|
|
}
|
|
} else {
|
|
quote::quote! {
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct #key {
|
|
#(#fields),*
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut out = syn::parse2::<syn::File>(tokens)
|
|
.expect("Failed to parse struct")
|
|
.items;
|
|
out.extend(extra_structs);
|
|
out
|
|
}
|