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, } #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct Schema { #[serde(rename = "type")] _type: Types, properties: Option>, #[serde(rename = "oneOf")] one_of: Option>, #[serde(rename = "enum")] _enum: Option>, description: Option, } #[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, nullable: Option, format: Option, items: Option>, properties: Option>, #[serde(rename = "additionalProperties")] additional_properties: Option>, #[serde(rename = "enum")] _enum: Option>, #[serde(rename = "allOf")] all_of: Option>, #[serde(rename = "oneOf")] one_of: Option>, description: Option, #[serde(rename = "$ref")] _ref: Option, } 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", properties.to_string() ) // } else if let Some(props) = &self.properties { // todo!() } else { "std::collections::HashMap".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 { 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 = jellyfin_openapi .components .schemas .iter() .filter(|(_k, v)| v.properties.is_some()) .map(|(k, v)| (k.clone(), v.clone())) .collect(); let enums: IndexMap = jellyfin_openapi .components .schemas .iter() .filter(|(_, v)| v._enum.is_some()) .map(|(k, v)| (k.clone(), v.clone())) .collect(); let syn_structs: Vec = 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::>(); 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::>(); 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, value: &Schema) -> Vec { 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::>(); 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!(">", 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![pub](proc_macro2::Span::call_site())), 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::>(); 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::(tokens) .expect("Failed to parse struct") .items; out.extend(extra_structs); out }