wip(tesla_api_coverage): start to compare api
This commit is contained in:
parent
d0b8f6df67
commit
3f7df753b1
|
@ -14,3 +14,6 @@ tracing = "0.1.40"
|
|||
log = "0.4.20"
|
||||
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"
|
|
@ -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<String, Call>,
|
||||
calls: HashMap<String, FleetEndpoint>,
|
||||
}
|
||||
|
||||
// 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<Scope>,
|
||||
parameters: Vec<Parameter>,
|
||||
request_example: String,
|
||||
response_example: String,
|
||||
// description: String,
|
||||
// category: Category,
|
||||
// scopes: Vec<Scope>,
|
||||
// parameters: Vec<Parameter>,
|
||||
// request_example: String,
|
||||
// response_example: String,
|
||||
}
|
||||
|
||||
pub fn parse(html: &str) -> () {
|
||||
pub fn parse(html: &str) -> HashMap<String, FleetEndpoint> {
|
||||
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<Call> {
|
||||
fn parse_call(element: ElementRef) -> Option<FleetEndpoint> {
|
||||
let name = element.value().id().unwrap();
|
||||
|
||||
// <p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/auto_conditioning_start</code></span></p>
|
||||
|
@ -122,7 +130,6 @@ fn parse_call(element: ElementRef) -> Option<Call> {
|
|||
}
|
||||
|
||||
let (method, url) = url.split_once(' ').unwrap();
|
||||
println!("{} {}", method, url);
|
||||
|
||||
// <p>scopes: <em>vehicle_cmds</em></p>
|
||||
let (fragment, element) = next(element);
|
||||
|
@ -156,7 +163,11 @@ fn parse_call(element: ElementRef) -> Option<Call> {
|
|||
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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
21
tesla_api_coverage/src/timdorr.rs
Normal file
21
tesla_api_coverage/src/timdorr.rs
Normal file
|
@ -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<String, Endpoint> {
|
||||
let map: HashMap<String, Endpoint> = 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::<HashMap<String, Endpoint>>()
|
||||
}
|
|
@ -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<String, VehicleCommandEndpoint> {
|
||||
// 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.
|
||||
|
|
Loading…
Reference in a new issue