use heck::ToKebabCase; use scraper::{Element, ElementRef, Html, Selector}; use std::collections::HashMap; use std::str::FromStr; use tracing::debug; struct FleetApiSpec { calls: HashMap, } // e.g. serialize to similar: vehicle-endpoints #[derive(Debug, strum::EnumString)] #[strum(serialize_all = "kebab-case")] enum Category { ChargingEndpoints, PartnerEndpoints, UserEndpoints, VehicleCommands, VehicleEndpoints, } #[derive(Debug, strum::EnumString)] #[strum(serialize_all = "snake_case")] enum Scope { /// Profile Information /// /// Contact information, home address, profile picture, and referral information. UserData, /// Vehicle Information /// /// Vehicle live data, location, eligible upgrades, nearby superchargers, ownership, and service scheduling data. VehicleDeviceData, /// Vehicle Commands /// /// Commands like add/remove driver, access Live Camera, unlock, wake up, remote start, and schedule software updates. VehicleCmds, /// Vehicle Charging Management /// /// Vehicle charging history, billed amount, charging location, commands to schedule, and start/stop charging. VehicleChargingCmds, /// Energy Product Information /// /// Energy flow history, saving forecast, tariff rates, grid import, calendar, site status, time of use, and ownership. EnergyDeviceData, /// Energy Product Commands /// /// Commands like update storm mode. EnergyCmds, } enum InRequestData { Query, Body, } pub struct Parameter { name: String, request: InRequestData, var_type: String, required: bool, description: String, } #[derive(Debug)] pub struct FleetEndpoint { name: String, method: reqwest::Method, url_definition: String, // description: String, // category: Category, // scopes: Vec, // parameters: Vec, // request_example: String, // response_example: String, } pub fn parse(html: &str) -> HashMap { let document = Html::parse_document(html); let content_selector = selector(".content h1"); let mut element = document.select(&content_selector).next().unwrap(); let mut category = None; let mut map = HashMap::with_capacity(100); // Iterate over all the elements in the content section until we see a h1 or h2. loop { match element.value().name() { "h1" => { let category_name = element.value().id().unwrap(); category = Category::from_str(&category_name).ok(); } "h2" => { if category.is_some() { let name = element.inner_html().to_kebab_case(); let call = parse_call(element); if let Some(endpoint) = call { debug!("{category:?} {endpoint:?}"); map.insert(name, endpoint); } } } _ => {} } let Some(next_element) = element.next_sibling_element() else { return map; }; element = next_element; } } /// Return None if this is not an endpoint. /// /// Will panic if it looks like an endpoint and has trouble parsing. fn parse_call(element: ElementRef) -> Option { let name = element.value().id().unwrap(); //

POST /api/1/vehicles/{id}/command/auto_conditioning_start

// This section determines if this is an endpoint or not. let (fragment, element) = next(element); let url = fragment.select(&selector("code")).next()?.inner_html(); if !url.starts_with("GET ") && !url.starts_with("POST ") { return None; } let (method, url) = url.split_once(' ').unwrap(); //

scopes: vehicle_cmds

let (fragment, element) = next(element); let scopes = fragment .select(&selector("em")) .map(|e| e.inner_html()) .map(|e| Scope::from_str(&e)) .collect::>(); // 4
nodes containing example requests in different languages. // TODO: Skip for now let mut count = 0; let mut element = element; loop { let (fragment, new_element) = next(element); element = new_element; if fragment .select(&selector(r#"div[class="highlight"]"#)) .next() .is_none() { break; } count += 1; if count == 10 { panic!("Too many examples"); } } if count == 0 && name != "api-status" { panic!("No examples for {}", name); } Some(FleetEndpoint { name: name.to_string(), method: reqwest::Method::from_bytes(method.as_bytes()).unwrap(), url_definition: url.to_string(), }) } fn next(element: ElementRef) -> (Html, ElementRef) { let element = element.next_sibling_element().unwrap(); let html = Html::parse_fragment(&element.html()); (html, element) } fn selector(s: &str) -> Selector { Selector::parse(s).unwrap() }