diff --git a/tesla_api_coverage/Cargo.toml b/tesla_api_coverage/Cargo.toml index f310f96..a60b16b 100644 --- a/tesla_api_coverage/Cargo.toml +++ b/tesla_api_coverage/Cargo.toml @@ -16,4 +16,6 @@ nom = "7.1.3" 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 +heck = "0.5.0-rc.1" +glob = "0.3.1" +syn = { version = "2.0.38" , features = ["full", "extra-traits"] } \ No newline at end of file diff --git a/tesla_api_coverage/src/main.rs b/tesla_api_coverage/src/main.rs index f6dcc39..5714add 100644 --- a/tesla_api_coverage/src/main.rs +++ b/tesla_api_coverage/src/main.rs @@ -1,4 +1,6 @@ mod fleet; +mod nom_help; +mod teslatte; mod timdorr; mod vehicle_command; @@ -35,21 +37,26 @@ async fn main() { cache_fetch(VEHICLE_COMMAND_URL, VEHICLE_COMMAND_FILE, args.cached) ); - let timdorr_endpoints = timdorr::parse(&timdorr); - let fleet_endpoints = fleet::parse(&fleet_html); - let vehicle_command_endpoints = vehicle_command::parse(&command_golang); + // let timdorr_endpoints = timdorr::parse(&timdorr); + // let fleet_endpoints = fleet::parse(&fleet_html); + // let vehicle_command_endpoints = vehicle_command::parse(&command_golang); - // 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(); + let mut teslatte_project_path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).unwrap(); + teslatte_project_path.push(".."); - let timdorr_only = timdorr_keys.difference(&fleet_keys); - let fleet_only = fleet_keys.difference(&timdorr_keys); - let both = timdorr_keys.intersection(&fleet_keys); + let teslatte_endpoints = teslatte::parse(&teslatte_project_path); - info!("Timdorr only: {:?}", timdorr_only); - info!("Fleet only: {:?}", fleet_only); - info!("Both: {:?}", both); + // // 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(); + // + // let timdorr_only = timdorr_keys.difference(&fleet_keys); + // let fleet_only = fleet_keys.difference(&timdorr_keys); + // let both = timdorr_keys.intersection(&fleet_keys); + // + // 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/nom_help.rs b/tesla_api_coverage/src/nom_help.rs new file mode 100644 index 0000000..87b049d --- /dev/null +++ b/tesla_api_coverage/src/nom_help.rs @@ -0,0 +1,39 @@ +use nom::branch::alt; +use nom::bytes::complete::{tag, take_until}; +use nom::character::complete::{char, line_ending, space1}; +use nom::multi::many0; +use nom::IResult; +use tracing::trace; + +/// Ignore the quotes and return the inner string. +/// e.g. "unlock" +pub fn quoted_string(s: &str) -> IResult<&str, &str> { + short_trace("quoted string", s); + let (s, _) = char('"')(s)?; + let (s, string) = take_until("\"")(s)?; + let (s, _) = char('"')(s)?; + Ok((s, string)) +} + +pub fn ignore_whitespace(s: &str) -> IResult<&str, ()> { + short_trace("ignore whitespace", s); + let (s, ws) = many0(alt((tag("\t"), space1, line_ending)))(s)?; + short_trace("ignore whitespace afterwards", s); + Ok((s, ())) +} + +pub fn until_eol(s: &str) -> IResult<&str, &str> { + short_trace("eol", s); + let (s, line) = take_until("\n")(s)?; + let (s, _) = line_ending(s)?; + short_trace("eol afterwards", s); + Ok((s, line)) +} + +pub fn short_trace(prefix: &str, s: &str) { + let mut max_len_left = 20; + if s.len() < max_len_left { + max_len_left = s.len(); + } + trace!("{}: {:?}...", prefix, &s[0..max_len_left]) +} diff --git a/tesla_api_coverage/src/teslatte.rs b/tesla_api_coverage/src/teslatte.rs new file mode 100644 index 0000000..95239c6 --- /dev/null +++ b/tesla_api_coverage/src/teslatte.rs @@ -0,0 +1,243 @@ +//! Parse the whole teslatte project and find any get*! post*! macros. + +use crate::nom_help::{ignore_whitespace, quoted_string, short_trace}; +use nom::branch::alt; +use nom::bytes::complete::{tag, take_till1, take_until1, take_while1}; +use nom::character::complete::alpha1; +use nom::character::is_alphabetic; +use nom::combinator::opt; +use nom::{IResult, Needed}; +use reqwest::Method; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use tracing::{debug, info, trace}; + +#[derive(Debug)] +pub struct TeslatteEndpoint { + pub method: Method, + pub endpoint: String, + pub uri: String, + // pub args: Vec, + // pub post_struct: Option, +} + +pub fn parse(path: &Path) -> anyhow::Result> { + // glob all .rs files from path + + let mut path = PathBuf::from(path); + path.push("src"); + path.push("**/*.rs"); + + debug!("Globbing {path:?}"); + + let pattern = path.to_str().unwrap(); + for file in glob::glob(pattern).unwrap() { + let path = file?; + + if !path.ends_with("src/vehicles.rs") { + continue; + } + + parse_file(&path)?; + } + + Ok(todo!()) +} + +/// Examples +/// +/// impl VehicleApi for OwnerApi { +/// get!(vehicles, Vec, "/vehicles"); +/// get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); +/// post_arg_empty!(wake_up, "/vehicles/{}/command/wake_up", VehicleId); +/// +/// Another one: +/// +/// impl OwnerApi { +/// pub_get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); +/// pub_get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); +/// } +/// +fn parse_file(path: &PathBuf) -> anyhow::Result<()> { + info!("Parsing file: {path:?}"); + let content = read_to_string(path)?; + + let mut endpoints = vec![]; + let mut inside_owner_api = false; + + for line in content.lines() { + let line = line.trim(); + trace!(?line); + + let owner_api_start = is_owner_api_start(line); + if owner_api_start { + if inside_owner_api { + panic!("Nested OwnerApi") + } + + trace!("Found OwnerApi"); + inside_owner_api = true; + continue; + } + + if line == "}" && inside_owner_api { + trace!("End OwnerApi"); + inside_owner_api = false; + continue; + } + + if !inside_owner_api { + continue; + } + + trace!("Looking at line: {line:?}"); + let (_, maybe_endpoint) = opt(alt((get, get_arg)))(line).unwrap(); + if let Some(endpoint) = maybe_endpoint { + endpoints.push(endpoint); + } + } + + dbg!(endpoints); + + Ok(()) +} + +fn is_owner_api_start(line: &str) -> bool { + line.ends_with("OwnerApi {") +} + +// fn common_macro_with_comma<'a>(expected_tag: &str, s: &'a str) -> IResult<&'a str, &'a str> { +// short_trace("common macro", s); +// let (s, _) = ignore_whitespace(s)?; +// let (s, _) = tag(expected_tag)(s)?; +// let (s, _) = tag("(")(s)?; +// let (s, fn_name) = function_name(s)?; +// let (s, ()) = comma(s)?; +// +// Ok((s, fn_name)) +// } + +fn macro_fn_name_then_comma(expected_tag: &str) -> impl Fn(&str) -> IResult<&str, &str> + '_ { + return move |s: &str| -> IResult<&str, &str> { + short_trace("common macro", s); + let (s, _) = ignore_whitespace(s)?; + let (s, _) = tag(expected_tag)(s)?; + let (s, _) = tag("(")(s)?; + let (s, fn_name) = function_name(s)?; + let (s, ()) = comma(s)?; + + Ok((s, fn_name)) + }; +} + +/// get!(vehicles, Vec, "/vehicles"); +/// pub_get!(vehicles, Vec, "/vehicles"); +fn get(s: &str) -> IResult<&str, TeslatteEndpoint> { + let (s, fn_name) = alt(( + macro_fn_name_then_comma("get!"), + macro_fn_name_then_comma("pub_get!"), + ))(s)?; + + let (s, response_type) = struct_name(s)?; + let (s, ()) = comma(s)?; + let (s, uri) = quoted_string(s)?; + let (s, _) = end_args(s)?; + + let endpoint = TeslatteEndpoint { + method: Method::GET, + endpoint: fn_name.to_string(), + uri: uri.to_string(), + }; + + Ok((s, endpoint)) +} + +/// get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId); +fn get_arg(s: &str) -> IResult<&str, TeslatteEndpoint> { + let (s, fn_name) = alt(( + macro_fn_name_then_comma("get_arg!"), + macro_fn_name_then_comma("pub_get_arg!"), + ))(s)?; + let (s, response_type) = struct_name(s)?; + let (s, ()) = comma(s)?; + let (s, uri) = quoted_string(s)?; + let (s, ()) = comma(s)?; + let (s, arg_type) = struct_name(s)?; + let (s, _) = end_args(s)?; + + let endpoint = TeslatteEndpoint { + method: Method::GET, + endpoint: fn_name.to_string(), + uri: uri.to_string(), + }; + + Ok((s, endpoint)) +} + +fn function_name(s: &str) -> IResult<&str, &str> { + take_while1(is_function_chars)(s) +} + +fn struct_name(s: &str) -> IResult<&str, &str> { + let (s, name) = take_while1(is_type)(s)?; + + Ok((s, name)) +} + +fn is_function_chars(c: char) -> bool { + is_lower_alpha(c) || c == '_' +} + +fn is_lower_alpha(c: char) -> bool { + c.is_ascii_lowercase() +} + +fn is_alpha(c: char) -> bool { + c.is_ascii_alphabetic() +} + +fn is_type(c: char) -> bool { + c.is_ascii_alphabetic() || c == '<' || c == '>' +} + +fn comma(s: &str) -> IResult<&str, ()> { + let (s, _) = tag(",")(s)?; + let (s, _) = ignore_whitespace(s)?; + + Ok((s, ())) +} + +fn end_args(s: &str) -> IResult<&str, ()> { + let (s, _) = tag(");")(s)?; + Ok((s, ())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get() { + let s = r#"get!(vehicles, Vec, "/vehicles");"#; + let (_, endpoint) = get(s).unwrap(); + } + + #[test] + fn test_pub_get() { + let s = r#"pub_get!(vehicles, Vec, "/vehicles");"#; + let (_, endpoint) = get(s).unwrap(); + } + + #[test] + fn test_get_arg() { + let s = r#"get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId);"#; + let (_, endpoint) = get_arg(s).unwrap(); + } + + #[test] + fn test_pub_get_arg() { + let s = + r#"pub_get_arg!(vehicle_data, VehicleData, "/vehicles/{}/vehicle_data", VehicleId);"#; + let (_, endpoint) = get_arg(s).unwrap(); + } +} diff --git a/tesla_api_coverage/src/timdorr.rs b/tesla_api_coverage/src/timdorr.rs index 18c4f1f..614985f 100644 --- a/tesla_api_coverage/src/timdorr.rs +++ b/tesla_api_coverage/src/timdorr.rs @@ -4,18 +4,18 @@ use std::collections::{BTreeMap, HashMap}; #[derive(Debug, Deserialize)] #[serde(rename_all = "UPPERCASE")] -pub struct Endpoint { +pub struct TimdorrEndpoint { #[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(); +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::>() + .collect::>() } diff --git a/tesla_api_coverage/src/vehicle_command.rs b/tesla_api_coverage/src/vehicle_command.rs index 9334e26..596e28c 100644 --- a/tesla_api_coverage/src/vehicle_command.rs +++ b/tesla_api_coverage/src/vehicle_command.rs @@ -1,11 +1,11 @@ +use crate::nom_help; use nom::branch::alt; -use nom::bytes::complete::{tag, take_until, take_while}; -use nom::character::complete::{char, line_ending, space0, space1, tab}; +use nom::bytes::complete::{tag, take_until}; use nom::combinator::opt; -use nom::multi::{many0, many1}; +use nom::multi::many1; use nom::IResult; use std::collections::HashMap; -use tracing::{debug, trace, warn}; +use tracing::{debug, trace}; pub fn parse(s: &str) -> HashMap { // Seek all the way to: var commands = map[string]*Command{\n @@ -21,12 +21,12 @@ pub fn parse(s: &str) -> HashMap { } pub fn seek_to_map(s: &str) -> IResult<&str, ()> { - short_trace("seek to map", s); + nom_help::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); + nom_help::short_trace("seek to map done", s); Ok((s, ())) } @@ -52,40 +52,40 @@ fn map_entry(s: &str) -> IResult<&str, VehicleCommandEndpoint> { // }, // }, - short_trace("--- map entry ---", s); + nom_help::short_trace("--- map entry ---", s); // endpoint - short_trace("endpoint", s); - let (s, _) = ignore_whitespace(s)?; - let (s, endpoint) = quoted_string(s)?; - let (s, _) = until_eol(s)?; + nom_help::short_trace("endpoint", s); + let (s, _) = nom_help::ignore_whitespace(s)?; + let (s, endpoint) = nom_help::quoted_string(s)?; + let (s, _) = nom_help::until_eol(s)?; // help - short_trace("help", s); - let (s, _) = ignore_whitespace(s)?; + nom_help::short_trace("help", s); + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("help:")(s)?; - let (s, _) = ignore_whitespace(s)?; - let (s, help) = quoted_string(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; + let (s, help) = nom_help::quoted_string(s)?; let (s, _) = tag(",")(s)?; // requiresAuth - short_trace("requiresAuth", s); + nom_help::short_trace("requiresAuth", s); let (s, requires_auth) = bool_field_or_false(s, "requiresAuth:")?; // requiresFleetAPI - short_trace("requiresFleetAPI", s); + nom_help::short_trace("requiresFleetAPI", s); let (s, requires_fleet) = bool_field_or_false(s, "requiresFleetAPI:")?; // Required args - short_trace("required args", s); + nom_help::short_trace("required args", s); let (s, required_args) = args(s, "args: []Argument{")?; // Optional args - short_trace("optional args", s); + nom_help::short_trace("optional args", s); let (s, optional_args) = args(s, "optional: []Argument{")?; // Ignore the handler, as there's not really much data we can take out of it. - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = take_until("},")(s)?; let (s, _) = tag("},")(s)?; @@ -104,49 +104,24 @@ fn map_entry(s: &str) -> IResult<&str, VehicleCommandEndpoint> { Ok((s, map_entry)) } -/// Ignore the quotes and return the inner string. -/// e.g. "unlock" -fn quoted_string(s: &str) -> IResult<&str, &str> { - short_trace("quoted string", s); - let (s, _) = char('"')(s)?; - let (s, string) = take_until("\"")(s)?; - let (s, _) = char('"')(s)?; - Ok((s, string)) -} - -fn ignore_whitespace(s: &str) -> IResult<&str, ()> { - short_trace("ignore whitespace", s); - let (s, ws) = many0(alt((tag("\t"), space1, line_ending)))(s)?; - short_trace("ignore whitespace afterwards", s); - Ok((s, ())) -} - -fn until_eol(s: &str) -> IResult<&str, &str> { - short_trace("eol", s); - let (s, line) = take_until("\n")(s)?; - let (s, _) = line_ending(s)?; - short_trace("eol afterwards", s); - Ok((s, line)) -} - fn str_to_bool(s: &str) -> IResult<&str, bool> { - short_trace("bool", s); + nom_help::short_trace("bool", s); let (s, bool_str) = alt((tag("true"), tag("false")))(s)?; let bool = match bool_str { "true" => true, "false" => false, _ => unreachable!(), }; - short_trace("bool afterwards", s); + nom_help::short_trace("bool afterwards", s); Ok((s, bool)) } /// If the field isn't there, assume false. fn bool_field<'a>(field_tag: &str) -> impl Fn(&'a str) -> IResult<&'a str, bool> + '_ { return move |s: &str| -> IResult<&'a str, bool> { - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag(field_tag)(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, value) = str_to_bool(s)?; let (s, _) = tag(",")(s)?; @@ -165,41 +140,41 @@ struct Arg { } fn args<'a>(s: &'a str, field_tag: &str) -> IResult<&'a str, Vec> { - short_trace("args", s); + nom_help::short_trace("args", s); - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, maybe_field) = opt(tag(field_tag))(s)?; if maybe_field.is_none() { trace!("no arg record"); return Ok((s, vec![])); } - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, args) = many1(arg)(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("},")(s)?; - short_trace("args afterwards", s); + nom_help::short_trace("args afterwards", s); Ok((s, args)) } fn arg(s: &str) -> IResult<&str, Arg> { - short_trace("arg", s); - let (s, _) = ignore_whitespace(s)?; + nom_help::short_trace("arg", s); + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("Argument{")(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("name:")(s)?; - let (s, _) = ignore_whitespace(s)?; - let (s, name) = quoted_string(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; + let (s, name) = nom_help::quoted_string(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag(",")(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("help:")(s)?; - let (s, _) = ignore_whitespace(s)?; - let (s, help) = quoted_string(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; + let (s, help) = nom_help::quoted_string(s)?; let (s, _) = opt(tag(","))(s)?; - let (s, _) = ignore_whitespace(s)?; + let (s, _) = nom_help::ignore_whitespace(s)?; let (s, _) = tag("},")(s)?; - short_trace("arg afterwards", s); + nom_help::short_trace("arg afterwards", s); Ok(( s, Arg { @@ -208,11 +183,3 @@ fn arg(s: &str) -> IResult<&str, Arg> { }, )) } - -fn short_trace(prefix: &str, s: &str) { - let mut max_len_left = 20; - if s.len() < max_len_left { - max_len_left = s.len(); - } - trace!("{}: {:?}...", prefix, &s[0..max_len_left]) -}