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"
|
log = "0.4.20"
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
anyhow = "1.0.75"
|
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 scraper::{Element, ElementRef, Html, Selector};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
struct FleetApiSpec {
|
struct FleetApiSpec {
|
||||||
calls: HashMap<String, Call>,
|
calls: HashMap<String, FleetEndpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// e.g. serialize to similar: vehicle-endpoints
|
// e.g. serialize to similar: vehicle-endpoints
|
||||||
|
@ -56,7 +58,7 @@ enum InRequestData {
|
||||||
Body,
|
Body,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Parameter {
|
pub struct Parameter {
|
||||||
name: String,
|
name: String,
|
||||||
request: InRequestData,
|
request: InRequestData,
|
||||||
var_type: String,
|
var_type: String,
|
||||||
|
@ -64,24 +66,27 @@ struct Parameter {
|
||||||
description: String,
|
description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Call {
|
#[derive(Debug)]
|
||||||
|
pub struct FleetEndpoint {
|
||||||
name: String,
|
name: String,
|
||||||
method: reqwest::Method,
|
method: reqwest::Method,
|
||||||
url_definition: String,
|
url_definition: String,
|
||||||
description: String,
|
// description: String,
|
||||||
category: Category,
|
// category: Category,
|
||||||
scopes: Vec<Scope>,
|
// scopes: Vec<Scope>,
|
||||||
parameters: Vec<Parameter>,
|
// parameters: Vec<Parameter>,
|
||||||
request_example: String,
|
// request_example: String,
|
||||||
response_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 document = Html::parse_document(html);
|
||||||
let content_selector = selector(".content h1");
|
let content_selector = selector(".content h1");
|
||||||
let mut element = document.select(&content_selector).next().unwrap();
|
let mut element = document.select(&content_selector).next().unwrap();
|
||||||
let mut category = None;
|
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.
|
// Iterate over all the elements in the content section until we see a h1 or h2.
|
||||||
loop {
|
loop {
|
||||||
match element.value().name() {
|
match element.value().name() {
|
||||||
|
@ -91,17 +96,20 @@ pub fn parse(html: &str) -> () {
|
||||||
}
|
}
|
||||||
"h2" => {
|
"h2" => {
|
||||||
if category.is_some() {
|
if category.is_some() {
|
||||||
let name = element.inner_html();
|
let name = element.inner_html().to_kebab_case();
|
||||||
println!("{category:?} {name:?}");
|
let call = parse_call(element);
|
||||||
// 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 {
|
let Some(next_element) = element.next_sibling_element() else {
|
||||||
println!("exiting...");
|
return map;
|
||||||
break;
|
|
||||||
};
|
};
|
||||||
element = next_element;
|
element = next_element;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +118,7 @@ pub fn parse(html: &str) -> () {
|
||||||
/// Return None if this is not an endpoint.
|
/// Return None if this is not an endpoint.
|
||||||
///
|
///
|
||||||
/// Will panic if it looks like an endpoint and has trouble parsing.
|
/// 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();
|
let name = element.value().id().unwrap();
|
||||||
|
|
||||||
// <p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/auto_conditioning_start</code></span></p>
|
// <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();
|
let (method, url) = url.split_once(' ').unwrap();
|
||||||
println!("{} {}", method, url);
|
|
||||||
|
|
||||||
// <p>scopes: <em>vehicle_cmds</em></p>
|
// <p>scopes: <em>vehicle_cmds</em></p>
|
||||||
let (fragment, element) = next(element);
|
let (fragment, element) = next(element);
|
||||||
|
@ -156,7 +163,11 @@ fn parse_call(element: ElementRef) -> Option<Call> {
|
||||||
panic!("No examples for {}", name);
|
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) {
|
fn next(element: ElementRef) -> (Html, ElementRef) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
mod fleet;
|
mod fleet;
|
||||||
|
mod timdorr;
|
||||||
mod vehicle_command;
|
mod vehicle_command;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
use scraper::Element;
|
use scraper::Element;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -21,9 +22,6 @@ struct Cli {
|
||||||
/// Use the cached html if exists, to avoid making requests.
|
/// Use the cached html if exists, to avoid making requests.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
cached: bool,
|
cached: bool,
|
||||||
|
|
||||||
#[clap(short = 'v', long)]
|
|
||||||
only_vehicle_command: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -31,43 +29,27 @@ async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
|
||||||
// let timorr = cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cache).await;
|
let (timdorr, fleet_html, command_golang) = tokio::join!(
|
||||||
//
|
|
||||||
// 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!(
|
|
||||||
cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cached),
|
cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cached),
|
||||||
cache_fetch(FLEET_API_URL, FLEET_API_FILE, args.cached),
|
cache_fetch(FLEET_API_URL, FLEET_API_FILE, args.cached),
|
||||||
cache_fetch(VEHICLE_COMMAND_URL, VEHICLE_COMMAND_FILE, args.cached)
|
cache_fetch(VEHICLE_COMMAND_URL, VEHICLE_COMMAND_FILE, args.cached)
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut vehicle_command = true;
|
let timdorr_endpoints = timdorr::parse(&timdorr);
|
||||||
let mut fleet_api = true;
|
let fleet_endpoints = fleet::parse(&fleet_html);
|
||||||
let mut timdorr = true;
|
let vehicle_command_endpoints = vehicle_command::parse(&command_golang);
|
||||||
|
|
||||||
if args.only_vehicle_command {
|
// Make hashsets from all the keys and see what's different between timdorr and fleet
|
||||||
fleet_api = false;
|
let timdorr_keys: std::collections::HashSet<&String> = timdorr_endpoints.keys().collect();
|
||||||
timdorr = false;
|
let fleet_keys: std::collections::HashSet<&String> = fleet_endpoints.keys().collect();
|
||||||
}
|
|
||||||
|
|
||||||
if fleet_api {
|
let timdorr_only = timdorr_keys.difference(&fleet_keys);
|
||||||
fleet::parse(&fleet_html);
|
let fleet_only = fleet_keys.difference(&timdorr_keys);
|
||||||
}
|
let both = timdorr_keys.intersection(&fleet_keys);
|
||||||
|
|
||||||
if vehicle_command {
|
info!("Timdorr only: {:?}", timdorr_only);
|
||||||
vehicle_command::parse(&command_golang);
|
info!("Fleet only: {:?}", fleet_only);
|
||||||
}
|
info!("Both: {:?}", both);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cache_fetch(url: &str, filename: &str, cache: bool) -> String {
|
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::combinator::opt;
|
||||||
use nom::multi::{many0, many1};
|
use nom::multi::{many0, many1};
|
||||||
use nom::IResult;
|
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
|
// Seek all the way to: var commands = map[string]*Command{\n
|
||||||
// Afterwards has the first map entry.
|
// Afterwards has the first map entry.
|
||||||
let commands_start = "var commands = map[string]*Command{\n";
|
let (s, _) = seek_to_map(s).unwrap();
|
||||||
let offset = s.find(commands_start).unwrap();
|
|
||||||
let s = &s[offset + commands_start.len()..];
|
|
||||||
|
|
||||||
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)]
|
#[derive(Debug)]
|
||||||
struct MapEntry {
|
pub struct VehicleCommandEndpoint {
|
||||||
endpoint: String,
|
pub endpoint: String,
|
||||||
help: String,
|
pub help: String,
|
||||||
// requires_auth: bool,
|
pub requires_auth: bool,
|
||||||
// requires_fleet: bool,
|
pub requires_fleet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_entry(s: &str) -> IResult<&str, MapEntry> {
|
fn map_entry(s: &str) -> IResult<&str, VehicleCommandEndpoint> {
|
||||||
// "unlock": &Command{
|
// "unlock": &Command{
|
||||||
// help: "Unlock vehicle",
|
// help: "Unlock vehicle",
|
||||||
// requiresAuth: true,
|
// requiresAuth: true,
|
||||||
|
@ -66,17 +76,16 @@ fn map_entry(s: &str) -> IResult<&str, MapEntry> {
|
||||||
short_trace("requiresFleetAPI", s);
|
short_trace("requiresFleetAPI", s);
|
||||||
let (s, requires_fleet) = bool_field_or_false(s, "requiresFleetAPI:")?;
|
let (s, requires_fleet) = bool_field_or_false(s, "requiresFleetAPI:")?;
|
||||||
|
|
||||||
// required args
|
// Required args
|
||||||
short_trace("required args", s);
|
short_trace("required args", s);
|
||||||
let (s, required_args) = args(s, "args: []Argument{")?;
|
let (s, required_args) = args(s, "args: []Argument{")?;
|
||||||
|
|
||||||
// optional args
|
// Optional args
|
||||||
short_trace("optional args", s);
|
short_trace("optional args", s);
|
||||||
let (s, optional_args) = args(s, "optional: []Argument{")?;
|
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, _) = ignore_whitespace(s)?;
|
||||||
|
|
||||||
let (s, _) = take_until("},")(s)?;
|
let (s, _) = take_until("},")(s)?;
|
||||||
let (s, _) = tag("},")(s)?;
|
let (s, _) = tag("},")(s)?;
|
||||||
|
|
||||||
|
@ -84,15 +93,15 @@ fn map_entry(s: &str) -> IResult<&str, MapEntry> {
|
||||||
let (s, _) = take_until("},")(s)?;
|
let (s, _) = take_until("},")(s)?;
|
||||||
let (s, _) = tag("},")(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((
|
Ok((s, map_entry))
|
||||||
s,
|
|
||||||
MapEntry {
|
|
||||||
endpoint: endpoint.to_string(),
|
|
||||||
help: help.to_string(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ignore the quotes and return the inner string.
|
/// Ignore the quotes and return the inner string.
|
||||||
|
|
Loading…
Reference in a new issue