wip(tesla_api_coverage): start to compare api

This commit is contained in:
gak 2023-10-23 16:17:50 +11:00
parent d0b8f6df67
commit 3f7df753b1
No known key found for this signature in database
5 changed files with 104 additions and 78 deletions

View file

@ -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"

View file

@ -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) {

View file

@ -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 {

View 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>>()
}

View file

@ -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.