wip(tesla_api_coverage): more massaging of endpoints

This commit is contained in:
gak 2023-10-25 12:24:36 +11:00
parent d66f15a91e
commit 9d4d7804a2
No known key found for this signature in database
8 changed files with 179 additions and 32 deletions

6
API.md
View file

@ -11,6 +11,10 @@ List of all known Tesla APIs, and if this crate supports it, and which of the Te
Currently only the Owner API is partially supported by this crate. Currently only the Owner API is partially supported by this crate.
<!-- tesla_api_coverage start table -->
| API | Owner API | Fleet API | Command Mode | | API | Owner API | Fleet API | Command Mode |
|-----------| --- | --- | --- | | --- | --- | --- | --- |
| honk_horn | ✅ | | | | honk_horn | ✅ | | |
<!-- tesla_api_coverage end table -->

View file

@ -14,6 +14,11 @@ This project does (or will do) the following:
* Has a configuration on how to merge the endpoints, e.g. if an endpoint name is different, how to resolve it. * Has a configuration on how to merge the endpoints, e.g. if an endpoint name is different, how to resolve it.
* Output a table of endpoints that are implemented or not, maybe in Markdown. * Output a table of endpoints that are implemented or not, maybe in Markdown.
### API.md output
* Update API.md in the root.
* Look for `<!-- tesla_api_coverage start table -->` and `<!-- tesla_api_coverage end table -->` and generate the table in between.
* Use timdorr's API as the source of truth for the list of endpoints because it is the oldest and longest!
### Brainstorm ### Brainstorm

View file

@ -0,0 +1,14 @@
use crate::fleet::FleetEndpoint;
use crate::teslatte::TeslatteEndpoint;
use crate::timdorr::TimdorrEndpoint;
use crate::vehicle_command::VehicleCommandEndpoint;
use std::collections::HashMap;
pub fn generate_api_md(
teslatte: Vec<TeslatteEndpoint>,
fleet: HashMap<String, FleetEndpoint>,
vehicle_command: HashMap<String, VehicleCommandEndpoint>,
timdorr: HashMap<String, TimdorrEndpoint>,
) -> anyhow::Result<()> {
todo!()
}

View file

@ -66,11 +66,11 @@ pub struct Parameter {
description: String, description: String,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct FleetEndpoint { pub struct FleetEndpoint {
name: String, pub name: String,
method: reqwest::Method, pub method: reqwest::Method,
url_definition: String, pub uri: String,
// description: String, // description: String,
// category: Category, // category: Category,
// scopes: Vec<Scope>, // scopes: Vec<Scope>,
@ -109,10 +109,19 @@ pub fn parse(html: &str) -> HashMap<String, FleetEndpoint> {
} }
let Some(next_element) = element.next_sibling_element() else { let Some(next_element) = element.next_sibling_element() else {
return map; break;
}; };
element = next_element; element = next_element;
} }
// Replace all vehicles/{id}/ to use {vehicle_id}
for (name, endpoint) in &mut map {
endpoint.uri = endpoint
.uri
.replace("vehicles/{id}/", "vehicles/{vehicle_id}/");
}
return map;
} }
/// Return None if this is not an endpoint. /// Return None if this is not an endpoint.
@ -166,7 +175,7 @@ fn parse_call(element: ElementRef) -> Option<FleetEndpoint> {
Some(FleetEndpoint { Some(FleetEndpoint {
name: name.to_string(), name: name.to_string(),
method: reqwest::Method::from_bytes(method.as_bytes()).unwrap(), method: reqwest::Method::from_bytes(method.as_bytes()).unwrap(),
url_definition: url.to_string(), uri: url.to_string(),
}) })
} }

View file

@ -1,14 +1,19 @@
mod api_md;
mod fleet; mod fleet;
mod nom_help; mod nom_help;
mod teslatte; mod teslatte;
mod timdorr; mod timdorr;
mod vehicle_command; mod vehicle_command;
use clap::{Parser, Subcommand}; use crate::fleet::FleetEndpoint;
use scraper::Element; use crate::teslatte::TeslatteEndpoint;
use crate::timdorr::TimdorrEndpoint;
use crate::vehicle_command::VehicleCommandEndpoint;
use clap::Parser;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tracing::info; use tracing::{error, info};
const TIMDORR_URL: &str = const TIMDORR_URL: &str =
"https://raw.githubusercontent.com/timdorr/tesla-api/master/ownerapi_endpoints.json"; "https://raw.githubusercontent.com/timdorr/tesla-api/master/ownerapi_endpoints.json";
@ -40,6 +45,10 @@ async fn main() {
let mut teslatte_project_path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).unwrap(); let mut teslatte_project_path = PathBuf::from_str(env!("CARGO_MANIFEST_DIR")).unwrap();
teslatte_project_path.push(".."); teslatte_project_path.push("..");
let teslatte_endpoints = teslatte::parse(&teslatte_project_path).unwrap(); let teslatte_endpoints = teslatte::parse(&teslatte_project_path).unwrap();
let teslatte_endpoints: HashMap<String, TeslatteEndpoint> = teslatte_endpoints
.into_iter()
.map(|e| (e.name.clone(), e))
.collect();
let fleet_endpoints = fleet::parse(&fleet_html); let fleet_endpoints = fleet::parse(&fleet_html);
let command_endpoints = vehicle_command::parse(&command_golang); let command_endpoints = vehicle_command::parse(&command_golang);
@ -49,6 +58,98 @@ async fn main() {
info!("{} endpoints in fleet", fleet_endpoints.len()); info!("{} endpoints in fleet", fleet_endpoints.len());
info!("{} endpoints in command", command_endpoints.len()); info!("{} endpoints in command", command_endpoints.len());
info!("{} endpoints in timdorr", timdorr_endpoints.len()); info!("{} endpoints in timdorr", timdorr_endpoints.len());
let mut merged = merge(
teslatte_endpoints,
fleet_endpoints,
command_endpoints,
timdorr_endpoints,
)
.unwrap();
// Let's do a check to see if the fleet api matches the timdorr api.
// If it doesn't, let's panic!
let mut perfect = true;
for (k, v) in &merged {
if let Some(fleet) = &v.fleet {
if let Some(timdorr) = &v.timdorr {
if fleet.uri != timdorr.uri {
error!("{}: fleet: {}, timdorr: {}", k, fleet.uri, timdorr.uri);
perfect = false;
}
}
}
}
if !perfect {
panic!("Fleet and Timdorr don't match. See errors above.");
}
// filter_interesting_endpoints(&mut merged);
todo!();
dbg!(&merged);
}
/// Remove endpoints that we're not interested in (yet) in place.
// pub fn filter_interesting_endpoints(mut endpoints: &mut HashMap<String, Endpoint>) {
// endpoints.retain(|_, e| {
// !e. starts_with("/api/1/directives")
// && !e.starts_with("/api/1/subscriptions")
// && !e.starts_with("/api/1/dx/")
// && !e.starts_with("/bff/v2/mobile-app")
// });
// }
#[derive(Debug)]
pub struct Endpoint {
pub name: String,
pub teslatte: Option<TeslatteEndpoint>,
pub fleet: Option<FleetEndpoint>,
pub vehicle_command: Option<VehicleCommandEndpoint>,
pub timdorr: Option<TimdorrEndpoint>,
}
pub fn merge(
teslatte: HashMap<String, TeslatteEndpoint>,
fleet: HashMap<String, FleetEndpoint>,
vehicle_command: HashMap<String, VehicleCommandEndpoint>,
timdorr: HashMap<String, TimdorrEndpoint>,
) -> anyhow::Result<HashMap<String, Endpoint>> {
// Collate all the keys into a single set
let mut keys = HashSet::with_capacity(100);
keys.extend(teslatte.iter().map(|(k, _)| k.clone()));
keys.extend(fleet.keys().map(|k| k.clone()));
keys.extend(vehicle_command.keys().map(|k| k.clone()));
keys.extend(timdorr.keys().map(|k| k.clone()));
// Put the keys into a Vec and sort.
let mut keys: Vec<String> = keys.into_iter().collect();
keys.sort();
let mut endpoints = Vec::with_capacity(keys.len());
for name in keys {
// for each of these maps, if the key exists, then we have an endpoint.
let teslatte = teslatte.get(&name).cloned();
let fleet = fleet.get(&name).cloned();
let vehicle_command = vehicle_command.get(&name).cloned();
let timdorr = timdorr.get(&name).cloned();
let endpoint = Endpoint {
name,
teslatte,
fleet,
vehicle_command,
timdorr,
};
endpoints.push(endpoint);
}
Ok(endpoints
.into_iter()
.map(|e| (e.name.clone(), e))
.collect::<HashMap<String, Endpoint>>())
} }
async fn cache_fetch(url: &str, filename: &str, cache: bool) -> String { async fn cache_fetch(url: &str, filename: &str, cache: bool) -> String {

View file

@ -1,21 +1,20 @@
//! Parse the whole teslatte project and find any get*! post*! macros. //! Parse the whole teslatte project and find any get*! post*! macros.
use crate::nom_help::{ignore_whitespace, quoted_string, short_trace}; use crate::nom_help::{ignore_whitespace, quoted_string, short_trace};
use heck::ToKebabCase;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::{tag, take_till1, take_until1, take_while1}; use nom::bytes::complete::{tag, take_while1};
use nom::character::complete::alpha1;
use nom::character::is_alphabetic;
use nom::combinator::opt; use nom::combinator::opt;
use nom::{IResult, Needed}; use nom::IResult;
use reqwest::Method; use reqwest::Method;
use std::fs::read_to_string; use std::fs::read_to_string;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::{debug, info, trace}; use tracing::{debug, trace};
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct TeslatteEndpoint { pub struct TeslatteEndpoint {
pub method: Method, pub method: Method,
pub endpoint: String, pub name: String,
pub uri: String, pub uri: String,
// pub args: Vec<String>, // pub args: Vec<String>,
// pub post_struct: Option<String>, // pub post_struct: Option<String>,
@ -24,7 +23,8 @@ pub struct TeslatteEndpoint {
pub fn parse(path: &Path) -> anyhow::Result<Vec<TeslatteEndpoint>> { pub fn parse(path: &Path) -> anyhow::Result<Vec<TeslatteEndpoint>> {
let mut path = PathBuf::from(path); let mut path = PathBuf::from(path);
path.push("src"); path.push("src");
path.push("**/*.rs"); path.push("**");
path.push("*.rs");
debug!("Globbing {path:?}"); debug!("Globbing {path:?}");
@ -37,6 +37,11 @@ pub fn parse(path: &Path) -> anyhow::Result<Vec<TeslatteEndpoint>> {
endpoints.extend(append_endpoints); endpoints.extend(append_endpoints);
} }
// Change the name to be kebab-case.
for endpoint in &mut endpoints {
endpoint.name = endpoint.name.to_kebab_case();
}
Ok(endpoints) Ok(endpoints)
} }
@ -129,7 +134,7 @@ fn get(s: &str) -> IResult<&str, TeslatteEndpoint> {
let endpoint = TeslatteEndpoint { let endpoint = TeslatteEndpoint {
method: Method::GET, method: Method::GET,
endpoint: fn_name.to_string(), name: fn_name.to_string(),
uri: uri.to_string(), uri: uri.to_string(),
}; };
@ -151,7 +156,7 @@ fn get_arg(s: &str) -> IResult<&str, TeslatteEndpoint> {
let endpoint = TeslatteEndpoint { let endpoint = TeslatteEndpoint {
method: Method::GET, method: Method::GET,
endpoint: fn_name.to_string(), name: fn_name.to_string(),
uri: uri.to_string(), uri: uri.to_string(),
}; };
@ -173,7 +178,7 @@ fn get_args(s: &str) -> IResult<&str, TeslatteEndpoint> {
let endpoint = TeslatteEndpoint { let endpoint = TeslatteEndpoint {
method: Method::GET, method: Method::GET,
endpoint: fn_name.to_string(), name: fn_name.to_string(),
uri: uri.to_string(), uri: uri.to_string(),
}; };
@ -195,7 +200,7 @@ fn post_arg(s: &str) -> IResult<&str, TeslatteEndpoint> {
let endpoint = TeslatteEndpoint { let endpoint = TeslatteEndpoint {
method: Method::POST, method: Method::POST,
endpoint: fn_name.to_string(), name: fn_name.to_string(),
uri: uri.to_string(), uri: uri.to_string(),
}; };
@ -216,7 +221,7 @@ fn post_arg_empty(s: &str) -> IResult<&str, TeslatteEndpoint> {
let endpoint = TeslatteEndpoint { let endpoint = TeslatteEndpoint {
method: Method::POST, method: Method::POST,
endpoint: fn_name.to_string(), name: fn_name.to_string(),
uri: uri.to_string(), uri: uri.to_string(),
}; };

View file

@ -1,20 +1,29 @@
use heck::ToKebabCase; use heck::ToKebabCase;
use serde::Deserialize; use serde::Deserialize;
use std::collections::{BTreeMap, HashMap}; use std::collections::HashMap;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "UPPERCASE")] #[serde(rename_all = "UPPERCASE")]
pub struct TimdorrEndpoint { pub struct TimdorrEndpoint {
#[serde(rename = "TYPE")] #[serde(rename = "TYPE")]
method: String, pub method: String,
uri: String, pub uri: String,
auth: bool, pub auth: bool,
} }
pub fn parse(json_str: &str) -> HashMap<String, TimdorrEndpoint> { pub fn parse(json_str: &str) -> HashMap<String, TimdorrEndpoint> {
let map: HashMap<String, TimdorrEndpoint> = serde_json::from_str(json_str).unwrap(); let map: HashMap<String, TimdorrEndpoint> = serde_json::from_str(json_str).unwrap();
// rename all the keys to kebab-case // Massage all URLs to have a / before "api".
let map = map
.into_iter()
.map(|(k, mut v)| {
v.uri = format!("/{}", v.uri);
(k, v)
})
.collect::<HashMap<String, TimdorrEndpoint>>();
// Rename all the keys to kebab-case
map.into_iter() map.into_iter()
.map(|(k, v)| (k.to_kebab_case(), v)) .map(|(k, v)| (k.to_kebab_case(), v))
.collect::<HashMap<String, TimdorrEndpoint>>() .collect::<HashMap<String, TimdorrEndpoint>>()

View file

@ -30,7 +30,7 @@ pub fn seek_to_map(s: &str) -> IResult<&str, ()> {
Ok((s, ())) Ok((s, ()))
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct VehicleCommandEndpoint { pub struct VehicleCommandEndpoint {
pub endpoint: String, pub endpoint: String,
pub help: String, pub help: String,