diff --git a/tesla_api_coverage/Cargo.toml b/tesla_api_coverage/Cargo.toml index 62486a3..f310f96 100644 --- a/tesla_api_coverage/Cargo.toml +++ b/tesla_api_coverage/Cargo.toml @@ -13,4 +13,7 @@ tracing-subscriber = "0.3.17" tracing = "0.1.40" log = "0.4.20" nom = "7.1.3" -anyhow = "1.0.75" \ No newline at end of file +anyhow = "1.0.75" +serde = { version = "1.0.189", features = ["derive"] } +serde_json = "1.0.107" +heck = "0.5.0-rc.1" \ No newline at end of file diff --git a/tesla_api_coverage/src/fleet.rs b/tesla_api_coverage/src/fleet.rs index 3b26270..44e9da8 100644 --- a/tesla_api_coverage/src/fleet.rs +++ b/tesla_api_coverage/src/fleet.rs @@ -1,9 +1,11 @@ +use heck::ToKebabCase; use scraper::{Element, ElementRef, Html, Selector}; use std::collections::HashMap; use std::str::FromStr; +use tracing::debug; struct FleetApiSpec { - calls: HashMap, + calls: HashMap, } // e.g. serialize to similar: vehicle-endpoints @@ -56,7 +58,7 @@ enum InRequestData { Body, } -struct Parameter { +pub struct Parameter { name: String, request: InRequestData, var_type: String, @@ -64,24 +66,27 @@ struct Parameter { description: String, } -struct Call { +#[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, + // description: String, + // category: Category, + // scopes: Vec, + // parameters: Vec, + // request_example: String, + // response_example: String, } -pub fn parse(html: &str) -> () { +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() { @@ -91,17 +96,20 @@ pub fn parse(html: &str) -> () { } "h2" => { if category.is_some() { - let name = element.inner_html(); - println!("{category:?} {name:?}"); - // let call = parse_call(element); + 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 { - println!("exiting..."); - break; + return map; }; element = next_element; } @@ -110,7 +118,7 @@ pub fn parse(html: &str) -> () { /// 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 { +fn parse_call(element: ElementRef) -> Option { let name = element.value().id().unwrap(); //

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

@@ -122,7 +130,6 @@ fn parse_call(element: ElementRef) -> Option { } let (method, url) = url.split_once(' ').unwrap(); - println!("{} {}", method, url); //

scopes: vehicle_cmds

let (fragment, element) = next(element); @@ -156,7 +163,11 @@ fn parse_call(element: ElementRef) -> Option { panic!("No examples for {}", name); } - None + 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) { diff --git a/tesla_api_coverage/src/main.rs b/tesla_api_coverage/src/main.rs index 188da41..f6dcc39 100644 --- a/tesla_api_coverage/src/main.rs +++ b/tesla_api_coverage/src/main.rs @@ -1,7 +1,8 @@ mod fleet; +mod timdorr; mod vehicle_command; -use clap::Parser; +use clap::{Parser, Subcommand}; use scraper::Element; use std::path::PathBuf; use std::str::FromStr; @@ -21,9 +22,6 @@ struct Cli { /// Use the cached html if exists, to avoid making requests. #[clap(short, long)] cached: bool, - - #[clap(short = 'v', long)] - only_vehicle_command: bool, } #[tokio::main] @@ -31,43 +29,27 @@ async fn main() { tracing_subscriber::fmt::init(); let args = Cli::parse(); - // let timorr = cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cache).await; - // - // let fleet_html = cache_fetch( - // FLEET_API_URL, - // FLEET_API_FILE, - // args.cache, - // ) - // .await; - // - // let command_golang = cache_fetch( - // VEHICLE_COMMAND_URL, - // VEHICLE_COMMAND_FILE, - // args.cache, - // ).await; - - let (timorr, fleet_html, command_golang) = tokio::join!( + let (timdorr, fleet_html, command_golang) = tokio::join!( cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cached), cache_fetch(FLEET_API_URL, FLEET_API_FILE, args.cached), cache_fetch(VEHICLE_COMMAND_URL, VEHICLE_COMMAND_FILE, args.cached) ); - let mut vehicle_command = true; - let mut fleet_api = true; - let mut timdorr = true; + let timdorr_endpoints = timdorr::parse(&timdorr); + let fleet_endpoints = fleet::parse(&fleet_html); + let vehicle_command_endpoints = vehicle_command::parse(&command_golang); - if args.only_vehicle_command { - fleet_api = false; - timdorr = false; - } + // Make hashsets from all the keys and see what's different between timdorr and fleet + let timdorr_keys: std::collections::HashSet<&String> = timdorr_endpoints.keys().collect(); + let fleet_keys: std::collections::HashSet<&String> = fleet_endpoints.keys().collect(); - if fleet_api { - fleet::parse(&fleet_html); - } + let timdorr_only = timdorr_keys.difference(&fleet_keys); + let fleet_only = fleet_keys.difference(&timdorr_keys); + let both = timdorr_keys.intersection(&fleet_keys); - if vehicle_command { - vehicle_command::parse(&command_golang); - } + info!("Timdorr only: {:?}", timdorr_only); + info!("Fleet only: {:?}", fleet_only); + info!("Both: {:?}", both); } async fn cache_fetch(url: &str, filename: &str, cache: bool) -> String { diff --git a/tesla_api_coverage/src/timdorr.rs b/tesla_api_coverage/src/timdorr.rs new file mode 100644 index 0000000..18c4f1f --- /dev/null +++ b/tesla_api_coverage/src/timdorr.rs @@ -0,0 +1,21 @@ +use heck::ToKebabCase; +use serde::Deserialize; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct Endpoint { + #[serde(rename = "TYPE")] + method: String, + uri: String, + auth: bool, +} + +pub fn parse(json_str: &str) -> HashMap { + let map: HashMap = serde_json::from_str(json_str).unwrap(); + + // rename all the keys to kebab-case + map.into_iter() + .map(|(k, v)| (k.to_kebab_case(), v)) + .collect::>() +} diff --git a/tesla_api_coverage/src/vehicle_command.rs b/tesla_api_coverage/src/vehicle_command.rs index 4a62516..9334e26 100644 --- a/tesla_api_coverage/src/vehicle_command.rs +++ b/tesla_api_coverage/src/vehicle_command.rs @@ -4,31 +4,41 @@ use nom::character::complete::{char, line_ending, space0, space1, tab}; use nom::combinator::opt; use nom::multi::{many0, many1}; use nom::IResult; -use tracing::{trace, warn}; +use std::collections::HashMap; +use tracing::{debug, trace, warn}; -pub fn parse(s: &str) -> () { +pub fn parse(s: &str) -> HashMap { // Seek all the way to: var commands = map[string]*Command{\n // Afterwards has the first map entry. - let commands_start = "var commands = map[string]*Command{\n"; - let offset = s.find(commands_start).unwrap(); - let s = &s[offset + commands_start.len()..]; + let (s, _) = seek_to_map(s).unwrap(); - let (go, entries) = many1(map_entry)(s).unwrap(); + let (_, entries) = many1(map_entry)(s).unwrap(); - dbg!(&entries); + entries + .into_iter() + .map(|e| (e.endpoint.clone(), e)) + .collect() +} - warn!("todo: parse") +pub fn seek_to_map(s: &str) -> IResult<&str, ()> { + short_trace("seek to map", s); + let tag_str = "var commands = map[string]*Command{\n"; + // There's gotta be a nom function to these two lines. + let (s, _) = take_until(tag_str)(s)?; + let (s, _) = tag(tag_str)(s)?; + short_trace("seek to map done", s); + Ok((s, ())) } #[derive(Debug)] -struct MapEntry { - endpoint: String, - help: String, - // requires_auth: bool, - // requires_fleet: bool, +pub struct VehicleCommandEndpoint { + pub endpoint: String, + pub help: String, + pub requires_auth: bool, + pub requires_fleet: bool, } -fn map_entry(s: &str) -> IResult<&str, MapEntry> { +fn map_entry(s: &str) -> IResult<&str, VehicleCommandEndpoint> { // "unlock": &Command{ // help: "Unlock vehicle", // requiresAuth: true, @@ -66,17 +76,16 @@ fn map_entry(s: &str) -> IResult<&str, MapEntry> { short_trace("requiresFleetAPI", s); let (s, requires_fleet) = bool_field_or_false(s, "requiresFleetAPI:")?; - // required args + // Required args short_trace("required args", s); let (s, required_args) = args(s, "args: []Argument{")?; - // optional args + // Optional args short_trace("optional args", s); let (s, optional_args) = args(s, "optional: []Argument{")?; - // check and ignore the handler, as there's not really much data we can take out of it. + // Ignore the handler, as there's not really much data we can take out of it. let (s, _) = ignore_whitespace(s)?; - let (s, _) = take_until("},")(s)?; let (s, _) = tag("},")(s)?; @@ -84,15 +93,15 @@ fn map_entry(s: &str) -> IResult<&str, MapEntry> { let (s, _) = take_until("},")(s)?; let (s, _) = tag("},")(s)?; - dbg!(endpoint, help, requires_auth, requires_fleet); + let map_entry = VehicleCommandEndpoint { + endpoint: endpoint.to_string(), + help: help.to_string(), + requires_auth, + requires_fleet, + }; + debug!(?map_entry); - Ok(( - s, - MapEntry { - endpoint: endpoint.to_string(), - help: help.to_string(), - }, - )) + Ok((s, map_entry)) } /// Ignore the quotes and return the inner string.