use std::collections::HashMap;
use std::fs;
use std::path::Path;
use serde::Deserialize;
use ureq::Agent;
use crate::error::BuilderError;
pub trait DiscoveryFetcher: Send + Sync {
fn fetch_document(&self, service: &str, version: &str) -> Result<String, BuilderError>;
}
pub fn validate_api_identifier(s: &str) -> Result<(), BuilderError> {
if s.is_empty() {
return Err(BuilderError::Resolution(
"API identifier must not be empty".into(),
));
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
{
return Err(BuilderError::Resolution(format!(
"invalid API identifier {s:?}: only [a-zA-Z0-9._-] allowed"
)));
}
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiDirectoryItem {
name: String,
version: String,
#[serde(default)]
discovery_rest_url: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ApiDirectoryList {
#[serde(default)]
items: Vec<ApiDirectoryItem>,
}
fn encode_path_segment(s: &str) -> String {
s.to_string()
}
pub struct HttpFetcher {
agent: Agent,
}
impl Default for HttpFetcher {
fn default() -> Self {
Self {
agent: Agent::new_with_config(
ureq::config::Config::builder()
.timeout_global(Some(std::time::Duration::from_secs(120)))
.build(),
),
}
}
}
impl HttpFetcher {
pub fn new() -> Self {
Self::default()
}
fn fetch_directory(&self) -> Result<ApiDirectoryList, BuilderError> {
let url = "https://www.googleapis.com/discovery/v1/apis";
let body = self
.agent
.get(url)
.call()
.map_err(|e| BuilderError::Fetch {
service: "directory".into(),
version: "v1".into(),
source: Box::new(e),
})?
.into_body()
.read_to_string()
.map_err(|e| BuilderError::Fetch {
service: "directory".into(),
version: "v1".into(),
source: Box::new(e),
})?;
serde_json::from_str(&body).map_err(|e| BuilderError::Parse {
service: "directory".into(),
source: e,
})
}
fn find_discovery_url(
&self,
service: &str,
version: &str,
) -> Result<Option<String>, BuilderError> {
let list = self.fetch_directory()?;
Ok(list
.items
.into_iter()
.find(|i| i.name == service && i.version == version)
.and_then(|i| i.discovery_rest_url))
}
fn fetch_url(&self, url: &str, service: &str, version: &str) -> Result<String, BuilderError> {
self.agent
.get(url)
.call()
.map_err(|e| BuilderError::Fetch {
service: service.into(),
version: version.into(),
source: Box::new(e),
})?
.into_body()
.read_to_string()
.map_err(|e| BuilderError::Fetch {
service: service.into(),
version: version.into(),
source: Box::new(e),
})
}
}
impl DiscoveryFetcher for HttpFetcher {
fn fetch_document(&self, service: &str, version: &str) -> Result<String, BuilderError> {
validate_api_identifier(service)?;
validate_api_identifier(version)?;
let primary = format!(
"https://www.googleapis.com/discovery/v1/apis/{}/{}/rest",
encode_path_segment(service),
encode_path_segment(version)
);
let resp = self.agent.get(&primary).call();
let body = match resp {
Ok(r) => {
let status = r.status().as_u16();
if (200..300).contains(&status) {
r.into_body()
.read_to_string()
.map_err(|e| BuilderError::Fetch {
service: service.into(),
version: version.into(),
source: Box::new(e),
})?
} else {
if let Ok(Some(url)) = self.find_discovery_url(service, version) {
self.fetch_url(&url, service, version)?
} else {
let alt = format!(
"https://{service}.googleapis.com/$discovery/rest?version={version}"
);
self.fetch_url(&alt, service, version)?
}
}
}
Err(_) => {
if let Ok(Some(url)) = self.find_discovery_url(service, version) {
self.fetch_url(&url, service, version)?
} else {
let alt = format!(
"https://{service}.googleapis.com/$discovery/rest?version={version}"
);
self.fetch_url(&alt, service, version)?
}
}
};
Ok(body)
}
}
pub struct MapFetcher {
pub docs: HashMap<(String, String), String>,
}
impl DiscoveryFetcher for MapFetcher {
fn fetch_document(&self, service: &str, version: &str) -> Result<String, BuilderError> {
self.docs
.get(&(service.to_string(), version.to_string()))
.cloned()
.ok_or_else(|| {
BuilderError::Resolution(format!(
"MapFetcher: no document for {service}/{version}"
))
})
}
}
pub fn read_cache(cache_dir: &Path, service: &str, version: &str) -> Option<String> {
let path = cache_dir.join(format!("{service}_{version}.json"));
fs::read_to_string(path).ok()
}
pub fn write_cache(cache_dir: &Path, service: &str, version: &str, json: &str) {
if let Err(e) = fs::create_dir_all(cache_dir) {
eprintln!("gws-builder: could not create cache dir {}: {e}", cache_dir.display());
return;
}
let path = cache_dir.join(format!("{service}_{version}.json"));
if let Err(e) = fs::write(&path, json) {
eprintln!("gws-builder: could not write cache {}: {e}", path.display());
}
}