use crate::openapi::*; use eyre::WrapErr; use heck::ToPascalCase; use std::fmt::Write; pub fn create_structs(spec: &OpenApiV2) -> eyre::Result { let mut s = String::new(); s.push_str("use structs::*;\n"); s.push_str("pub mod structs {\n"); if let Some(definitions) = &spec.definitions { for (name, schema) in definitions { let strukt = create_struct_for_definition(&spec, name, schema)?; s.push_str(&strukt); } } for (_, item) in &spec.paths { let strukt = create_query_structs_for_path(item)?; s.push_str(&strukt); } s.push_str("\n}"); Ok(s) } pub fn create_struct_for_definition( spec: &OpenApiV2, name: &str, schema: &Schema, ) -> eyre::Result { if matches!(schema._type, Some(SchemaType::One(Primitive::Array))) { return Ok(String::new()); } let docs = create_struct_docs(schema)?; let mut fields = String::new(); let required = schema.required.as_deref().unwrap_or_default(); if let Some(properties) = &schema.properties { for (prop_name, prop_schema) in properties { let prop_ty = crate::schema_ref_type_name(spec, prop_schema)?; let field_name = crate::sanitize_ident(prop_name); let mut field_ty = prop_ty.clone(); if field_name.ends_with("url") && field_name != "ssh_url" && field_ty == "String" { field_ty = "url::Url".into() } if field_ty == name { field_ty = format!("Box<{field_ty}>") } if !required.contains(prop_name) { field_ty = format!("Option<{field_ty}>") } if field_ty == "Option" { fields.push_str("#[serde(deserialize_with = \"crate::none_if_blank_url\")]\n"); } if field_ty == "time::OffsetDateTime" { fields.push_str("#[serde(with = \"time::serde::rfc3339\")]\n"); } if field_ty == "Option" { fields.push_str("#[serde(with = \"time::serde::rfc3339::option\")]\n"); } if &field_name != prop_name { fields.push_str("#[serde(rename = \""); fields.push_str(prop_name); fields.push_str("\")]\n"); } fields.push_str("pub "); fields.push_str(&field_name); fields.push_str(": "); fields.push_str(&field_ty); fields.push_str(",\n"); } } if let Some(additonal_schema) = &schema.additional_properties { let prop_ty = crate::schema_ref_type_name(spec, additonal_schema)?; fields.push_str("#[serde(flatten)]\n"); fields.push_str("pub additional: std::collections::BTreeMap,\n"); } let out = format!("{docs}#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct {name} {{\n{fields}}}\n\n"); Ok(out) } fn create_struct_docs(schema: &Schema) -> eyre::Result { let doc = match &schema.description { Some(desc) => { let mut out = String::new(); for line in desc.lines() { out.push_str("/// "); out.push_str(line); out.push_str("\n/// \n"); } out } None => String::new(), }; Ok(doc) } pub fn create_query_structs_for_path(item: &PathItem) -> eyre::Result { let mut s = String::new(); if let Some(op) = &item.get { s.push_str(&create_query_struct(op).wrap_err("GET")?); } if let Some(op) = &item.put { s.push_str(&create_query_struct(op).wrap_err("PUT")?); } if let Some(op) = &item.post { s.push_str(&create_query_struct(op).wrap_err("POST")?); } if let Some(op) = &item.delete { s.push_str(&create_query_struct(op).wrap_err("DELETE")?); } if let Some(op) = &item.options { s.push_str(&create_query_struct(op).wrap_err("OPTIONS")?); } if let Some(op) = &item.head { s.push_str(&create_query_struct(op).wrap_err("HEAD")?); } if let Some(op) = &item.patch { s.push_str(&create_query_struct(op).wrap_err("PATCH")?); } Ok(s) } pub fn query_struct_name(op: &Operation) -> eyre::Result { let mut ty = op .operation_id .as_deref() .ok_or_else(|| eyre::eyre!("operation did not have id"))? .to_pascal_case() .replace("o_auth2", "oauth2"); ty.push_str("Query"); Ok(ty) } fn create_query_struct(op: &Operation) -> eyre::Result { let params = match &op.parameters { Some(params) => params, None => return Ok(String::new()), }; let mut fields = String::new(); let mut imp = String::new(); for param in params { let param = match ¶m { MaybeRef::Value { value } => value, MaybeRef::Ref { _ref } => eyre::bail!("todo: add deref parameters"), }; if let ParameterIn::Query { param: query_param } = ¶m._in { let ty = crate::methods::param_type(query_param, true)?; let field_name = crate::sanitize_ident(¶m.name); fields.push_str("pub "); fields.push_str(&field_name); fields.push_str(": "); if query_param.required { fields.push_str(&ty); } else { fields.push_str("Option<"); fields.push_str(&ty); fields.push_str(">"); } fields.push_str(",\n"); let mut handler = String::new(); if query_param.required { writeln!(&mut handler, "let {field_name} = &self.{field_name};")?; } else { writeln!( &mut handler, "if let Some({field_name}) = &self.{field_name} {{" )?; } match &query_param._type { ParameterType::String => match query_param.format.as_deref() { Some("date-time" | "date") => { writeln!( &mut handler, "write!(f, \"{}={{field_name}}&\", field_name = {field_name}.format(&time::format_description::well_known::Rfc3339).unwrap())?;", param.name)?; } _ => { writeln!( &mut handler, "write!(f, \"{}={{{}}}&\")?;", param.name, field_name.strip_prefix("r#").unwrap_or(&field_name) )?; } }, ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => { writeln!( &mut handler, "write!(f, \"{}={{{}}}&\")?;", param.name, field_name.strip_prefix("r#").unwrap_or(&field_name) )?; } ParameterType::Array => { let format = query_param .collection_format .unwrap_or(CollectionFormat::Csv); let item = query_param .items .as_ref() .ok_or_else(|| eyre::eyre!("array must have item type defined"))?; let item_pusher = match item._type { ParameterType::String => { match query_param.format.as_deref() { Some("date-time" | "date") => { "write!(f, \"{{date}}\", item.format(&time::format_description::well_known::Rfc3339).unwrap())?;" }, _ => { "write!(f, \"{item}\")?;" } } }, ParameterType::Number | ParameterType::Integer | ParameterType::Boolean => { "write!(f, \"{item}\")?;" }, ParameterType::Array => { eyre::bail!("nested arrays not supported in query"); }, ParameterType::File => eyre::bail!("cannot send file in query"), }; match format { CollectionFormat::Csv => { handler.push_str(&simple_query_array( param, item_pusher, &field_name, ",", )?); } CollectionFormat::Ssv => { handler.push_str(&simple_query_array( param, item_pusher, &field_name, " ", )?); } CollectionFormat::Tsv => { handler.push_str(&simple_query_array( param, item_pusher, &field_name, "\\t", )?); } CollectionFormat::Pipes => { handler.push_str(&simple_query_array( param, item_pusher, &field_name, "|", )?); } CollectionFormat::Multi => { writeln!(&mut handler)?; writeln!(&mut handler, "if !{field_name}.is_empty() {{")?; writeln!(&mut handler, "for item in {field_name} {{")?; writeln!(&mut handler, "write!(f, \"{}=\")?;", param.name)?; handler.push_str(item_pusher); handler.push('\n'); writeln!(&mut handler, "write!(f, \"&\")?;")?; writeln!(&mut handler, "}}")?; writeln!(&mut handler, "}}")?; } } } ParameterType::File => eyre::bail!("cannot send file in query"), } if !query_param.required { writeln!(&mut handler, "}}")?; } imp.push_str(&handler); } } let result = if fields.is_empty() { String::new() } else { let op_name = query_struct_name(op)?; format!( " pub struct {op_name} {{ {fields} }} impl std::fmt::Display for {op_name} {{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ {imp} Ok(()) }} }} " ) }; Ok(result) } fn simple_query_array( param: &Parameter, item_pusher: &str, name: &str, sep: &str, ) -> eyre::Result { let mut out = String::new(); writeln!( &mut out, " if !{name}.is_empty() {{ write!(f, \"{}=\")?; for (item, i) in {name}.iter().enumerate() {{ {item_pusher} if i < {name}.len() - 1 {{ write!(f, \"{sep}\")?; }} }} write!(f, \"&\")?; }}", param.name )?; Ok(out) }