/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::openapi::schema::OaSchema; use crate::oxidize::{CustomRustType, RustType}; use crate::{SUPPORTED_TYPES, simple_name}; /// Our representation of a Graph API property. #[derive(Debug, Clone)] pub struct Property { pub name: String, pub nullable: bool, pub is_collection: bool, pub rust_type: RustType, pub description: Option, /// Whether this property is an OpenAPI reference (not a Rust reference) pub is_ref: bool, } /// For the given schema object, extract its Graph API description and properties. pub fn extract_from_schema(schema: &OaSchema) -> (Option, Vec) { let mut out = Vec::new(); collect_schema_properties(schema, &mut out); (top_level_description(schema), out) } fn top_level_description(schema: &OaSchema) -> Option { match schema { OaSchema::Obj { description: Some(description), .. } => Some(description.clone()), OaSchema::Obj { all_of: Some(all_of), .. } => { for element in all_of { if let OaSchema::Obj { description: Some(description), .. } = element { return Some(description.clone()); } } None } _ => None, } } fn collect_schema_properties(schema: &OaSchema, out: &mut Vec) { match schema { OaSchema::Obj { navigation_property: true, .. } => { // navigation properties aren't real properties, they basically just inform about a subpath } OaSchema::Obj { all_of: Some(list), description, .. } => { for s in list { match s { OaSchema::Ref { reference } => { if let Some((name, ty)) = custom_from_ref(reference) { out.push(Property { name: name.to_string(), nullable: false, is_collection: false, is_ref: true, rust_type: ty, description: description.clone(), }); } } OaSchema::Obj { .. } => { collect_schema_properties(s, out); } } } } OaSchema::Obj { properties: Some(props), .. } => { for (name, prop_schema) in props { if let OaSchema::Obj { navigation_property: true, .. } = prop_schema { continue; } if name.starts_with("@odata.") { continue; } if let Some((is_collection, description, rust_type)) = map_openapi_schema_to_rust(prop_schema) { let nullable = prop_schema.nullable().unwrap_or(false); let is_ref = matches!(prop_schema, OaSchema::Ref { .. }); out.push(Property { name: name.clone(), nullable, is_collection, is_ref, rust_type, description, }); } else { println!("Skipping property with unsupported type: {name}"); } } } OaSchema::Obj { typ: Some(s), .. } | OaSchema::Ref { reference: s } => { // a direct single property (typically from inline definitions in paths) if let Some((is_collection, description, rust_type)) = map_openapi_schema_to_rust(schema) { let nullable = schema.nullable().unwrap_or(false); let is_ref = matches!(schema, OaSchema::Ref { .. }); out.push(Property { name: ref_simple_name(s).to_string(), nullable, is_collection, is_ref, rust_type, description, }); } else { println!("Skipping unsupported type: {s}"); } } _ => panic!("unknown schema structure: {schema:?}"), } } fn custom_from_ref(reference: &str) -> Option<(&str, RustType)> { let simple = ref_simple_name(reference); if SUPPORTED_TYPES.contains(&simple) { Some((simple, RustType::Custom(CustomRustType::from(simple)))) } else { None } } /// Given a reference in the shape `#/components/schemas/microsoft.graph.user`, /// get the name of the type being referred to. Note that the middle part is not /// always "schemas", e.g. `#/components/requestBodies/sendMailRequestBody`. fn ref_simple_name(reference: &str) -> &str { let name = std::path::Path::new(reference) .file_name() .expect("invalid ref name") .to_str() .expect("expected valid UTF-8 ref name"); simple_name(name) } fn map_openapi_schema_to_rust(schema: &OaSchema) -> Option<(bool, Option, RustType)> { match schema { OaSchema::Ref { reference } => { let simple = ref_simple_name(reference); if SUPPORTED_TYPES.contains(&simple) { Some((false, None, RustType::Custom(CustomRustType::from(simple)))) } else { println!("skipping unsupported schema: {simple}: {schema:?}"); None } } OaSchema::Obj { typ, format, items, description, .. } => { let description = description.clone(); if let Some(t) = typ.as_deref() { match t { "array" => { let item = items.as_deref()?; if let OaSchema::Obj { typ: Some(s), .. } = item && s == "array" { todo!("nested arrays: {schema:?}"); } let (_, _, typ) = map_openapi_schema_to_rust(item)?; Some((true, description, typ)) } "string" => Some(( false, description, map_string_format_to_rust(format.as_deref()), )), "boolean" => Some((false, description, RustType::Bool)), "integer" => Some(( false, description, map_integer_format_to_rust(format.as_deref()), )), "number" => Some(( false, description, map_number_format_to_rust(format.as_deref()), )), "object" => { if let Some(simple) = match_supported_custom_from_schema(schema) { Some(( false, description, RustType::Custom(CustomRustType::from(simple)), )) } else { panic!("Unrecognized 'object' schema: {schema:?}"); } } _ => None, } } else { if let Some(simple) = match_supported_custom_from_schema(schema) { return Some(( false, description, RustType::Custom(CustomRustType::from(simple)), )); } None } } } } // Try to discover a supported custom type by scanning refs inside composition. fn match_supported_custom_from_schema(schema: &OaSchema) -> Option { match schema { OaSchema::Ref { reference } => custom_simple_from_ref(reference), OaSchema::Obj { all_of, one_of, any_of, .. } => { for items in [all_of, one_of, any_of].into_iter().flatten() { for s in items { if let Some(found) = match_supported_custom_from_schema(s) { return Some(found); } } } None } } } fn custom_simple_from_ref(r#ref: &str) -> Option { let simple = ref_simple_name(r#ref).to_string(); if SUPPORTED_TYPES.contains(&simple.as_str()) { Some(simple) } else { None } } fn map_string_format_to_rust(fmt: Option<&str>) -> RustType { match fmt { None => RustType::String, Some("byte") | Some("binary") => todo!("base64 decoding"), Some(t) => { println!("treating {t} as a string"); RustType::String } } } fn map_integer_format_to_rust(fmt: Option<&str>) -> RustType { match fmt { Some("uint8") => RustType::U8, Some("int8") => RustType::I8, Some("int16") => RustType::I16, Some("int32") => RustType::I32, Some("int64") => RustType::I64, // Default to i32 if unspecified None => RustType::I32, Some(fmt) => panic!("Unknown number format: {fmt}"), } } fn map_number_format_to_rust(fmt: Option<&str>) -> RustType { match fmt { Some("uint8") => RustType::U8, Some("int8") => RustType::I8, Some("int16") => RustType::I16, Some("int32") => RustType::I32, Some("int64") => RustType::I64, Some("float") => RustType::F32, Some("double") => RustType::F64, Some("decimal") => RustType::F64, // technically lossy, but rarely used None => panic!("Number with unspecified format"), Some(fmt) => panic!("Unknown number format: {fmt}"), } }