Initial AS213905 SDK release
This commit is contained in:
+478
@@ -0,0 +1,478 @@
|
||||
use reqwest::{Method, StatusCode};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("AS213905 API error {status}: {code}: {message}")]
|
||||
Api {
|
||||
status: StatusCode,
|
||||
code: String,
|
||||
message: String,
|
||||
required_scope: Option<String>,
|
||||
},
|
||||
#[error("HTTP client error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("invalid base URL: {0}")]
|
||||
InvalidBaseUrl(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(api_key: impl Into<String>) -> Self {
|
||||
Self::with_base_url(api_key, "https://as213905.com").expect("built-in URL is valid")
|
||||
}
|
||||
pub fn with_base_url(
|
||||
api_key: impl Into<String>,
|
||||
base_url: impl AsRef<str>,
|
||||
) -> Result<Self, Error> {
|
||||
let parsed = reqwest::Url::parse(base_url.as_ref())
|
||||
.map_err(|e| Error::InvalidBaseUrl(e.to_string()))?;
|
||||
let http = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.user_agent("as213905-rust/1.1.0")
|
||||
.build()?;
|
||||
Ok(Self {
|
||||
api_key: api_key.into(),
|
||||
base_url: parsed.as_str().trim_end_matches('/').to_owned(),
|
||||
http,
|
||||
})
|
||||
}
|
||||
async fn request<T: DeserializeOwned, B: Serialize + ?Sized>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
org: Option<&str>,
|
||||
body: Option<&B>,
|
||||
) -> Result<T, Error> {
|
||||
let mut request = self
|
||||
.http
|
||||
.request(method, format!("{}{}", self.base_url, path))
|
||||
.bearer_auth(&self.api_key)
|
||||
.header("Accept", "application/json");
|
||||
if let Some(value) = org {
|
||||
request = request.header("X-Organization-ID", value);
|
||||
}
|
||||
if let Some(value) = body {
|
||||
request = request.json(value);
|
||||
}
|
||||
let response = request.send().await?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let value: APIErrorBody = response.json().await.unwrap_or_default();
|
||||
return Err(Error::Api {
|
||||
status,
|
||||
code: value.error.unwrap_or_else(|| "http_error".into()),
|
||||
message: value.message.unwrap_or_default(),
|
||||
required_scope: value.required_scope,
|
||||
});
|
||||
}
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
async fn empty<B: Serialize + ?Sized>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
org: Option<&str>,
|
||||
body: Option<&B>,
|
||||
) -> Result<(), Error> {
|
||||
let _: serde_json::Value = self.request(method, path, org, body).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn organization(&self) -> Result<Organization, Error> {
|
||||
Ok(self
|
||||
.request::<OrganizationResponse, serde_json::Value>(
|
||||
Method::GET,
|
||||
"/api/v1/organization",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.organization)
|
||||
}
|
||||
pub async fn invoices(&self) -> Result<Vec<Invoice>, Error> {
|
||||
Ok(self
|
||||
.request::<InvoiceList, serde_json::Value>(Method::GET, "/api/v1/invoices", None, None)
|
||||
.await?
|
||||
.invoices)
|
||||
}
|
||||
pub async fn services(&self) -> Result<Vec<Service>, Error> {
|
||||
Ok(self
|
||||
.request::<ServiceList, serde_json::Value>(Method::GET, "/api/v1/services", None, None)
|
||||
.await?
|
||||
.services)
|
||||
}
|
||||
pub async fn service(&self, id: &str) -> Result<Service, Error> {
|
||||
Ok(self
|
||||
.request::<ServiceResponse, serde_json::Value>(
|
||||
Method::GET,
|
||||
&format!("/api/v1/services/{id}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.service)
|
||||
}
|
||||
pub async fn service_metrics(
|
||||
&self,
|
||||
id: &str,
|
||||
target: Option<&str>,
|
||||
range: Option<&str>,
|
||||
) -> Result<MetricSeries, Error> {
|
||||
let mut q = vec![];
|
||||
if let Some(v) = target {
|
||||
q.push(("target", v))
|
||||
};
|
||||
if let Some(v) = range {
|
||||
q.push(("range", v))
|
||||
};
|
||||
let s = serde_urlencoded::to_string(q).unwrap_or_default();
|
||||
self.request::<MetricSeries, serde_json::Value>(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"/api/v1/services/{id}/metrics{}{}",
|
||||
if s.is_empty() { "" } else { "?" },
|
||||
s
|
||||
),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn power_service(
|
||||
&self,
|
||||
id: &str,
|
||||
action: PowerAction,
|
||||
) -> Result<StatusAction, Error> {
|
||||
self.request(
|
||||
Method::POST,
|
||||
&format!("/api/v1/services/{id}/power"),
|
||||
None,
|
||||
Some(&PowerRequest { action }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn traffic(
|
||||
&self,
|
||||
days: Option<u16>,
|
||||
from: Option<&str>,
|
||||
to: Option<&str>,
|
||||
) -> Result<Traffic, Error> {
|
||||
self.request::<Traffic, serde_json::Value>(
|
||||
Method::GET,
|
||||
&traffic_path("/api/v1/traffic", days, from, to),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn tunnels(&self) -> Result<Vec<Tunnel>, Error> {
|
||||
Ok(self
|
||||
.request::<TunnelList, serde_json::Value>(Method::GET, "/api/v1/tunnels", None, None)
|
||||
.await?
|
||||
.tunnels)
|
||||
}
|
||||
pub async fn create_tunnel(&self, input: &CreateTunnel) -> Result<Tunnel, Error> {
|
||||
Ok(self
|
||||
.request::<TunnelResponse, CreateTunnel>(
|
||||
Method::POST,
|
||||
"/api/v1/tunnels",
|
||||
None,
|
||||
Some(input),
|
||||
)
|
||||
.await?
|
||||
.tunnel)
|
||||
}
|
||||
pub async fn delete_tunnel(&self, id: &str) -> Result<(), Error> {
|
||||
self.empty::<serde_json::Value>(
|
||||
Method::DELETE,
|
||||
&format!("/api/v1/tunnels/{id}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn tunnel_traffic(
|
||||
&self,
|
||||
id: &str,
|
||||
days: Option<u16>,
|
||||
from: Option<&str>,
|
||||
to: Option<&str>,
|
||||
) -> Result<TunnelTraffic, Error> {
|
||||
self.request::<TunnelTraffic, serde_json::Value>(
|
||||
Method::GET,
|
||||
&traffic_path(&format!("/api/v1/tunnels/{id}/traffic"), days, from, to),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn geofeeds(&self, org: &str) -> Result<GeofeedList, Error> {
|
||||
self.request::<GeofeedList, serde_json::Value>(
|
||||
Method::GET,
|
||||
"/api/v1/geofeeds",
|
||||
Some(org),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn create_geofeed(&self, org: &str, name: &str) -> Result<GeofeedList, Error> {
|
||||
self.request(
|
||||
Method::POST,
|
||||
"/api/v1/geofeeds",
|
||||
Some(org),
|
||||
Some(&NameRequest { name }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn geofeed(&self, org: &str, id: &str) -> Result<Geofeed, Error> {
|
||||
Ok(self
|
||||
.request::<GeofeedResponse, serde_json::Value>(
|
||||
Method::GET,
|
||||
&format!("/api/v1/geofeeds/{id}"),
|
||||
Some(org),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.geofeed)
|
||||
}
|
||||
pub async fn rename_geofeed(&self, org: &str, id: &str, name: &str) -> Result<(), Error> {
|
||||
self.empty(
|
||||
Method::PATCH,
|
||||
&format!("/api/v1/geofeeds/{id}"),
|
||||
Some(org),
|
||||
Some(&NameRequest { name }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn delete_geofeed(&self, org: &str, id: &str) -> Result<(), Error> {
|
||||
self.empty::<serde_json::Value>(
|
||||
Method::DELETE,
|
||||
&format!("/api/v1/geofeeds/{id}"),
|
||||
Some(org),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn replace_geofeed_records(
|
||||
&self,
|
||||
org: &str,
|
||||
id: &str,
|
||||
records: &[GeofeedRecord],
|
||||
) -> Result<(), Error> {
|
||||
self.empty(
|
||||
Method::PUT,
|
||||
&format!("/api/v1/geofeeds/{id}/records"),
|
||||
Some(org),
|
||||
Some(&RecordsRequest { records }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn rotate_geofeed_url(&self, org: &str, id: &str) -> Result<GeofeedList, Error> {
|
||||
self.request::<GeofeedList, serde_json::Value>(
|
||||
Method::POST,
|
||||
&format!("/api/v1/geofeeds/{id}/rotate"),
|
||||
Some(org),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn traffic_path(base: &str, days: Option<u16>, from: Option<&str>, to: Option<&str>) -> String {
|
||||
let mut q = vec![];
|
||||
if let Some(v) = days {
|
||||
q.push(("days", v.to_string()))
|
||||
};
|
||||
if let Some(v) = from {
|
||||
q.push(("from", v.to_owned()))
|
||||
};
|
||||
if let Some(v) = to {
|
||||
q.push(("to", v.to_owned()))
|
||||
};
|
||||
let s = serde_urlencoded::to_string(q).unwrap_or_default();
|
||||
format!("{base}{}{}", if s.is_empty() { "" } else { "?" }, s)
|
||||
}
|
||||
#[derive(Default, Deserialize)]
|
||||
struct APIErrorBody {
|
||||
error: Option<String>,
|
||||
message: Option<String>,
|
||||
required_scope: Option<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct OrganizationResponse {
|
||||
organization: Organization,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct InvoiceList {
|
||||
invoices: Vec<Invoice>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct ServiceList {
|
||||
services: Vec<Service>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct ServiceResponse {
|
||||
service: Service,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct TunnelList {
|
||||
tunnels: Vec<Tunnel>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct TunnelResponse {
|
||||
tunnel: Tunnel,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct GeofeedResponse {
|
||||
geofeed: Geofeed,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct PowerRequest {
|
||||
action: PowerAction,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct NameRequest<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct RecordsRequest<'a> {
|
||||
records: &'a [GeofeedRecord],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Organization {
|
||||
pub id: String,
|
||||
pub asn: u32,
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub commit_kbps: i64,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Invoice {
|
||||
pub id: String,
|
||||
pub number: String,
|
||||
pub status: String,
|
||||
pub currency: String,
|
||||
pub total_cents: i64,
|
||||
pub period_from: Option<String>,
|
||||
pub period_to: Option<String>,
|
||||
pub issued_at: Option<String>,
|
||||
pub due_at: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub label: String,
|
||||
pub status: String,
|
||||
pub monthly_cents: i64,
|
||||
pub next_due_on: Option<String>,
|
||||
#[serde(default)]
|
||||
pub controllable: bool,
|
||||
pub location: Option<String>,
|
||||
pub configuration: Option<serde_json::Value>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MetricPoint {
|
||||
pub t: String,
|
||||
pub v: Option<f64>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MetricSeries {
|
||||
pub metric: String,
|
||||
pub target: String,
|
||||
pub range: String,
|
||||
pub points: Vec<MetricPoint>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Period {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub label: String,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Traffic {
|
||||
pub p95_kbps: i64,
|
||||
#[serde(default)]
|
||||
pub commit_kbps: i64,
|
||||
#[serde(default)]
|
||||
pub over_commit: bool,
|
||||
pub total_bytes: i64,
|
||||
pub total_gb: f64,
|
||||
pub period: Period,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Tunnel {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub label: Option<String>,
|
||||
pub location: String,
|
||||
pub peer_endpoint: String,
|
||||
pub ifname: Option<String>,
|
||||
pub inner_v4: Option<String>,
|
||||
pub inner_v6: Option<String>,
|
||||
#[serde(default)]
|
||||
pub announce: bool,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TunnelTraffic {
|
||||
#[serde(flatten)]
|
||||
pub traffic: Traffic,
|
||||
pub tunnel: Tunnel,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreateTunnel {
|
||||
pub peer_endpoint: String,
|
||||
pub location: String,
|
||||
pub label: Option<String>,
|
||||
pub announce: Option<bool>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PowerAction {
|
||||
Start,
|
||||
Stop,
|
||||
Restart,
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct StatusAction {
|
||||
pub status: String,
|
||||
pub action: PowerAction,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeofeedRecord {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub id: String,
|
||||
pub prefix: String,
|
||||
pub country_code: String,
|
||||
#[serde(default)]
|
||||
pub region_code: String,
|
||||
#[serde(default)]
|
||||
pub city: String,
|
||||
#[serde(default)]
|
||||
pub postal_code: String,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Geofeed {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
#[serde(default)]
|
||||
pub record_count: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub records: Vec<GeofeedRecord>,
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GeofeedList {
|
||||
pub geofeeds: Vec<Geofeed>,
|
||||
pub limit: u8,
|
||||
}
|
||||
Reference in New Issue
Block a user