diff --git a/Cargo.toml b/Cargo.toml index b8d5deb..80f5538 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,3 +118,11 @@ path = "api/mason/sponsors.rs" [[bin]] name = "mason-renovate-badge" path = "api/mason/renovate/badge.rs" + +[[bin]] +name = "openvsx-versions-latest" +path = "api/openvsx/[namespace]/[extension]/versions/latest.rs" + +[[bin]] +name = "openvsx-versions-all" +path = "api/openvsx/[namespace]/[extension]/versions/all.rs" diff --git a/api/openvsx/[namespace]/[extension]/versions/all.rs b/api/openvsx/[namespace]/[extension]/versions/all.rs new file mode 100644 index 0000000..b0482f7 --- /dev/null +++ b/api/openvsx/[namespace]/[extension]/versions/all.rs @@ -0,0 +1,34 @@ +use http::{Method, StatusCode}; +use mason_registry_api::{ + openvsx::{client::OpenVSXClient, manager::OpenVSXManager}, + vercel::parse_url, + QueryParams, +}; +use vercel_runtime::{run, Body, Error, Request, Response}; + +async fn handler(request: Request) -> Result, Error> { + if request.method() != Method::GET { + return Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::Empty)?); + } + + let url = parse_url(&request)?; + let query_params: QueryParams = (&url).into(); + let extension = (&query_params).into(); + let manager = OpenVSXManager::new(OpenVSXClient::new()); + + match manager.get_all_versions(&extension) { + Ok(versions) => mason_registry_api::vercel::ok_json( + versions, + mason_registry_api::CacheControl::PublicMedium, + ), + Err(err) => mason_registry_api::vercel::err_json(err), + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + mason_registry_api::setup_tracing(); + run(handler).await +} diff --git a/api/openvsx/[namespace]/[extension]/versions/latest.rs b/api/openvsx/[namespace]/[extension]/versions/latest.rs new file mode 100644 index 0000000..fd0d3f2 --- /dev/null +++ b/api/openvsx/[namespace]/[extension]/versions/latest.rs @@ -0,0 +1,34 @@ +use http::{Method, StatusCode}; +use mason_registry_api::{ + openvsx::{api::OpenVSXExtensionResponse, client::OpenVSXClient, manager::OpenVSXManager}, + vercel::parse_url, + QueryParams, +}; +use vercel_runtime::{run, Body, Error, Request, Response}; + +async fn handler(request: Request) -> Result, Error> { + if request.method() != Method::GET { + return Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::Empty)?); + } + + let url = parse_url(&request)?; + let query_params: QueryParams = (&url).into(); + let extension = (&query_params).into(); + let manager = OpenVSXManager::new(OpenVSXClient::new()); + + match manager.get_extension(&extension) { + Ok(extension_dto) => mason_registry_api::vercel::ok_json::( + extension_dto.into(), + mason_registry_api::CacheControl::PublicMedium, + ), + Err(err) => mason_registry_api::vercel::err_json(err), + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + mason_registry_api::setup_tracing(); + run(handler).await +} diff --git a/src/golang/manager.rs b/src/golang/manager.rs index 73f2c09..02bf533 100644 --- a/src/golang/manager.rs +++ b/src/golang/manager.rs @@ -8,7 +8,7 @@ pub struct GolangManager { fn semver_sort_desc(a: &String, b: &String) -> Ordering { let a_semver = a.strip_prefix("v").unwrap_or(a).parse::(); - let b_semver = b.strip_prefix("v").unwrap_or(a).parse::(); + let b_semver = b.strip_prefix("v").unwrap_or(b).parse::(); if let (Ok(a), Ok(b)) = (&a_semver, &b_semver) { return b.cmp(a); } diff --git a/src/lib.rs b/src/lib.rs index feab4ba..33375b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod github; pub mod golang; pub mod http; pub mod npm; +pub mod openvsx; pub mod packagist; pub mod pypi; pub mod renovate; diff --git a/src/openvsx/api.rs b/src/openvsx/api.rs new file mode 100644 index 0000000..1fa569d --- /dev/null +++ b/src/openvsx/api.rs @@ -0,0 +1,18 @@ +use serde::Serialize; + +use super::client::spec::OpenVSXExtensionDto; + +#[derive(Serialize)] +pub struct OpenVSXExtensionResponse { + pub name: String, + pub version: String, +} + +impl From for OpenVSXExtensionResponse { + fn from(dto: OpenVSXExtensionDto) -> Self { + Self { + name: format!("{}/{}", dto.namespace, dto.name), + version: dto.version, + } + } +} diff --git a/src/openvsx/client/mod.rs b/src/openvsx/client/mod.rs new file mode 100644 index 0000000..552af76 --- /dev/null +++ b/src/openvsx/client/mod.rs @@ -0,0 +1,61 @@ +pub mod spec; + +use self::spec::{OpenVSXExtensionVersionsDto, OpenVSXExtensionDto}; + +use super::OpenVSXExtension; +use crate::http::client::{Client, HttpEndpoint}; +use std::fmt::Display; + +pub struct OpenVSXClient { + client: Client, +} + +enum OpenVSXEndpoint<'a> { + Extension(&'a OpenVSXExtension), + ExtensionVersions(&'a OpenVSXExtension), +} + +impl<'a> HttpEndpoint for OpenVSXEndpoint<'a> { + fn as_full_url(&self) -> String { + format!("https://open-vsx.org/api/{}", self) + } +} + +impl<'a> Display for OpenVSXEndpoint<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OpenVSXEndpoint::Extension(ext) => { + f.write_fmt(format_args!("{}/{}", ext.namespace, ext.extension)) + } + OpenVSXEndpoint::ExtensionVersions(ext) => { + f.write_fmt(format_args!("{}/{}/versions", ext.namespace, ext.extension)) + } + } + } +} + +impl OpenVSXClient { + pub fn new() -> Self { + Self { + client: Client::new(None), + } + } + + pub fn fetch_latest_extension_version( + &self, + extension: &OpenVSXExtension, + ) -> Result { + self.client + .get(OpenVSXEndpoint::Extension(extension))? + .json() + } + + pub fn fetch_extension_versions( + &self, + extension: &OpenVSXExtension, + ) -> Result { + self.client + .get(OpenVSXEndpoint::ExtensionVersions(extension))? + .json() + } +} diff --git a/src/openvsx/client/spec.rs b/src/openvsx/client/spec.rs new file mode 100644 index 0000000..2e02656 --- /dev/null +++ b/src/openvsx/client/spec.rs @@ -0,0 +1,15 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenVSXExtensionVersionsDto { + pub versions: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenVSXExtensionDto { + pub namespace: String, + pub name: String, + pub version: String, +} diff --git a/src/openvsx/errors.rs b/src/openvsx/errors.rs new file mode 100644 index 0000000..ab0892e --- /dev/null +++ b/src/openvsx/errors.rs @@ -0,0 +1,45 @@ +use http::StatusCode; +use thiserror::Error; + +use crate::errors::ApiError; + +#[derive(Error, Debug)] +pub enum OpenVSXError { + #[error("The requested resource was not found when interfacing with OpenVSX API.")] + ResourceNotFound { source: Option }, + #[error("Client error. {:?}", source.status())] + ClientError { source: reqwest::Error }, + #[error("OpenVSX API had a server error. {:?}", source.status())] + ServerError { source: reqwest::Error }, + #[error("Network error. {:?}", source.status())] + NetworkError { source: reqwest::Error }, +} + +impl ApiError for OpenVSXError { + fn status_code(&self) -> http::StatusCode { + match self { + OpenVSXError::ResourceNotFound { .. } => StatusCode::NOT_FOUND, + OpenVSXError::ClientError { .. } | OpenVSXError::NetworkError { .. } => { + StatusCode::INTERNAL_SERVER_ERROR + } + OpenVSXError::ServerError { .. } => StatusCode::BAD_GATEWAY, + } + } +} + +impl From for OpenVSXError { + fn from(req_error: reqwest::Error) -> Self { + match req_error.status() { + Some(reqwest::StatusCode::NOT_FOUND) => Self::ResourceNotFound { + source: Some(req_error), + }, + Some(status_code) if status_code.is_server_error() => { + Self::ServerError { source: req_error } + } + Some(status_code) if status_code.is_client_error() => { + Self::ClientError { source: req_error } + } + Some(_) | None => Self::NetworkError { source: req_error }, + } + } +} diff --git a/src/openvsx/manager.rs b/src/openvsx/manager.rs new file mode 100644 index 0000000..05f1e32 --- /dev/null +++ b/src/openvsx/manager.rs @@ -0,0 +1,49 @@ +use std::cmp::Ordering; + +use super::{ + client::{spec::OpenVSXExtensionDto, OpenVSXClient}, + errors::OpenVSXError, + OpenVSXExtension, +}; + +pub struct OpenVSXManager { + client: OpenVSXClient, +} + +fn semver_sort_desc(a: &String, b: &String) -> Ordering { + let a_semver = a.strip_prefix("v").unwrap_or(a).parse::(); + let b_semver = b.strip_prefix("v").unwrap_or(b).parse::(); + if let (Ok(a), Ok(b)) = (&a_semver, &b_semver) { + return b.cmp(a); + } + Ordering::Equal +} + +impl OpenVSXManager { + pub fn new(client: OpenVSXClient) -> Self { + Self { client } + } + + pub fn get_extension( + &self, + extension: &OpenVSXExtension, + ) -> Result { + Ok(self.client.fetch_latest_extension_version(extension)?) + } + + /// Returns all extension versions in DESCENDING order. + pub fn get_all_versions( + &self, + extension: &OpenVSXExtension, + ) -> Result, OpenVSXError> { + let mut unsorted_versions: Vec = self + .client + .fetch_extension_versions(extension)? + .versions + .into_keys() + .collect(); + + unsorted_versions.sort_by(semver_sort_desc); + Ok(unsorted_versions) + } +} diff --git a/src/openvsx/mod.rs b/src/openvsx/mod.rs new file mode 100644 index 0000000..f802b72 --- /dev/null +++ b/src/openvsx/mod.rs @@ -0,0 +1,27 @@ +use crate::QueryParams; + +pub mod api; +pub mod client; +pub mod errors; +pub mod manager; + +#[derive(Debug)] +pub struct OpenVSXExtension { + pub namespace: String, + pub extension: String, +} + +impl From<&QueryParams> for OpenVSXExtension { + fn from(query: &QueryParams) -> Self { + Self { + namespace: query + .get("namespace") + .expect("No [namespace] query param") + .to_owned(), + extension: query + .get("extension") + .expect("No [extension] query param") + .to_owned(), + } + } +} diff --git a/vercel.json b/vercel.json index 87f2766..f32d5bc 100644 --- a/vercel.json +++ b/vercel.json @@ -2,7 +2,7 @@ "ignoreCommand": "[ \"$VERCEL_ENV\" != production ]", "functions": { "api/**/*.rs": { - "runtime": "vercel-rust@4.0.0-beta.1" + "runtime": "vercel-rust@4.0.6" } }, "rewrites": [