diff --git a/registry/src/endpoints/package_version.rs b/registry/src/endpoints/package_version.rs index 4a48db6..9d1dd2f 100644 --- a/registry/src/endpoints/package_version.rs +++ b/registry/src/endpoints/package_version.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use actix_web::{http::header::ACCEPT, web, HttpRequest, HttpResponse, Responder}; use rusty_s3::{actions::GetObject, S3Action}; use semver::Version; @@ -38,38 +36,16 @@ impl<'de> Deserialize<'de> for VersionRequest { } } -#[derive(Debug)] -pub enum TargetRequest { - All, - Specific(TargetKind), -} - -impl<'de> Deserialize<'de> for TargetRequest { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - if s.eq_ignore_ascii_case("all") { - return Ok(TargetRequest::All); - } - - TargetKind::from_str(&s) - .map(TargetRequest::Specific) - .map_err(serde::de::Error::custom) - } -} - pub async fn get_package_version( request: HttpRequest, app_state: web::Data, - path: web::Path<(PackageName, VersionRequest, TargetRequest)>, + path: web::Path<(PackageName, VersionRequest, TargetKind)>, ) -> Result { let (name, version, target) = path.into_inner(); let (scope, name_part) = name.as_str(); - let versions: IndexFile = { + let entries: IndexFile = { let source = app_state.source.lock().unwrap(); match source.read_file([scope, name_part], &app_state.project, None)? { @@ -78,33 +54,17 @@ pub async fn get_package_version( } }; + let mut versions = entries.iter().filter(|(v_id, _)| *v_id.target() == target); + let version = match version { - VersionRequest::Latest => versions - .iter() - .filter(|(v_id, _)| match target { - TargetRequest::All => true, - TargetRequest::Specific(target) => *v_id.target() == target, - }) - .max_by_key(|(v, _)| v.version().clone()), - VersionRequest::Specific(version) => versions.iter().find(|(v, _)| { - *v.version() == version - && match target { - TargetRequest::All => true, - TargetRequest::Specific(target) => *v.target() == target, - } - }), + VersionRequest::Latest => versions.max_by_key(|(v, _)| v.version().clone()), + VersionRequest::Specific(version) => versions.find(|(v, _)| *v.version() == version), }; let Some((v_id, entry)) = version else { return Ok(HttpResponse::NotFound().finish()); }; - let other_targets = versions - .iter() - .filter(|(v, _)| v.version() == v_id.version() && v.target() != v_id.target()) - .map(|(v_id, _)| v_id.target().to_string()) - .collect::>(); - if request .headers() .get(ACCEPT) @@ -130,20 +90,19 @@ pub async fn get_package_version( )); } - let entry = entry.clone(); - - let mut response = serde_json::to_value(PackageResponse { + Ok(HttpResponse::Ok().json(PackageResponse { name: name.to_string(), version: v_id.version().to_string(), - target: entry.target.into(), - description: entry.description.unwrap_or_default(), - published_at: entry.published_at, - license: entry.license.unwrap_or_default(), - })?; - - if !other_targets.is_empty() { - response["other_targets"] = serde_json::to_value(other_targets)?; - } - - Ok(HttpResponse::Ok().json(response)) + targets: entries + .values() + .map(|entry| (&entry.target).into()) + .collect(), + description: entry.description.clone().unwrap_or_default(), + published_at: entries + .values() + .max_by_key(|entry| entry.published_at) + .unwrap() + .published_at, + license: entry.license.clone().unwrap_or_default(), + })) } diff --git a/registry/src/endpoints/package_versions.rs b/registry/src/endpoints/package_versions.rs index b81ef9e..8debee6 100644 --- a/registry/src/endpoints/package_versions.rs +++ b/registry/src/endpoints/package_versions.rs @@ -1,4 +1,5 @@ use actix_web::{web, HttpResponse, Responder}; +use std::collections::BTreeSet; use crate::{error::Error, package::PackageResponse, AppState}; use pesde::{ @@ -27,7 +28,7 @@ pub async fn get_package_versions( .map(|(v_id, entry)| PackageResponse { name: name.to_string(), version: v_id.version().to_string(), - target: entry.target.into(), + targets: BTreeSet::from([entry.target.into()]), description: entry.description.unwrap_or_default(), published_at: entry.published_at, license: entry.license.unwrap_or_default(), diff --git a/registry/src/endpoints/publish_version.rs b/registry/src/endpoints/publish_version.rs index 48cea73..dae0860 100644 --- a/registry/src/endpoints/publish_version.rs +++ b/registry/src/endpoints/publish_version.rs @@ -26,7 +26,7 @@ use pesde::{ use crate::{ auth::UserId, benv, - error::Error, + error::{Error, ErrorResponse}, package::{s3_name, S3_SIGN_DURATION}, search::update_version, AppState, @@ -208,6 +208,22 @@ pub async fn publish_package( dependencies, }; + let this_version = entries + .keys() + .find(|v_id| *v_id.version() == manifest.version); + if let Some(this_version) = this_version { + let other_entry = entries.get(this_version).unwrap(); + + if other_entry.description != new_entry.description + || other_entry.license != new_entry.license + { + return Ok(HttpResponse::BadRequest().json(ErrorResponse { + error: "same version with different description or license already exists" + .to_string(), + })); + } + } + if entries .insert( VersionId::new(manifest.version.clone(), manifest.target.kind()), diff --git a/registry/src/endpoints/search.rs b/registry/src/endpoints/search.rs index 42b6dae..66b588c 100644 --- a/registry/src/endpoints/search.rs +++ b/registry/src/endpoints/search.rs @@ -2,14 +2,15 @@ use std::collections::HashMap; use actix_web::{web, HttpResponse, Responder}; use serde::Deserialize; -use tantivy::{query::AllQuery, schema::Value, DateTime, Order}; +use tantivy::{collector::Count, query::AllQuery, schema::Value, DateTime, Order}; -use crate::{error::Error, package::PackageResponse, AppState}; use pesde::{ names::PackageName, source::{git_index::GitBasedSource, pesde::IndexFile}, }; +use crate::{error::Error, package::PackageResponse, AppState}; + #[derive(Deserialize)] pub struct Request { #[serde(default)] @@ -46,12 +47,15 @@ pub async fn search_packages( query_parser.parse_query(query)? }; - let top_docs = searcher + let (count, top_docs) = searcher .search( &query, - &tantivy::collector::TopDocs::with_limit(50) - .and_offset(request.offset.unwrap_or_default()) - .order_by_fast_field::("published_at", Order::Desc), + &( + Count, + tantivy::collector::TopDocs::with_limit(50) + .and_offset(request.offset.unwrap_or_default()) + .order_by_fast_field::("published_at", Order::Desc), + ), ) .unwrap(); @@ -71,7 +75,7 @@ pub async fn search_packages( .unwrap(); let (scope, name) = id.as_str(); - let mut versions: IndexFile = toml::de::from_str( + let versions: IndexFile = toml::de::from_str( &source .read_file([scope, name], &app_state.project, None) .unwrap() @@ -79,18 +83,32 @@ pub async fn search_packages( ) .unwrap(); - let (version_id, entry) = versions.pop_last().unwrap(); + let (latest_version, entry) = versions + .iter() + .max_by_key(|(v_id, _)| v_id.version()) + .unwrap(); PackageResponse { name: id.to_string(), - version: version_id.version().to_string(), - target: entry.target.into(), - description: entry.description.unwrap_or_default(), - published_at: entry.published_at, - license: entry.license.unwrap_or_default(), + version: latest_version.version().to_string(), + targets: versions + .iter() + .filter(|(v_id, _)| v_id.version() == latest_version.version()) + .map(|(_, entry)| (&entry.target).into()) + .collect(), + description: entry.description.clone().unwrap_or_default(), + published_at: versions + .values() + .max_by_key(|entry| entry.published_at) + .unwrap() + .published_at, + license: entry.license.clone().unwrap_or_default(), } }) .collect::>(); - Ok(HttpResponse::Ok().json(top_docs)) + Ok(HttpResponse::Ok().json(serde_json::json!({ + "data": top_docs, + "count": count, + }))) } diff --git a/registry/src/package.rs b/registry/src/package.rs index a7c7b76..f1e6d8b 100644 --- a/registry/src/package.rs +++ b/registry/src/package.rs @@ -5,7 +5,7 @@ use pesde::{ source::version_id::VersionId, }; use serde::Serialize; -use std::time::Duration; +use std::{collections::BTreeSet, time::Duration}; pub const S3_SIGN_DURATION: Duration = Duration::from_secs(60 * 60); @@ -13,7 +13,7 @@ pub fn s3_name(name: &PackageName, version_id: &VersionId) -> String { format!("{}+{}.tar.gz", name.escaped(), version_id.escaped()) } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Eq, PartialEq)] pub struct TargetInfo { kind: TargetKind, lib: bool, @@ -30,11 +30,33 @@ impl From for TargetInfo { } } +impl From<&Target> for TargetInfo { + fn from(target: &Target) -> Self { + TargetInfo { + kind: target.kind(), + lib: target.lib_path().is_some(), + bin: target.bin_path().is_some(), + } + } +} + +impl Ord for TargetInfo { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.kind.cmp(&other.kind) + } +} + +impl PartialOrd for TargetInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Debug, Serialize)] pub struct PackageResponse { pub name: String, pub version: String, - pub target: TargetInfo, + pub targets: BTreeSet, #[serde(skip_serializing_if = "String::is_empty")] pub description: String, pub published_at: DateTime, diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs index 0463dbb..49bc81d 100644 --- a/src/cli/commands/publish.rs +++ b/src/cli/commands/publish.rs @@ -406,34 +406,24 @@ impl PublishCommand { return Ok(()); } - match reqwest + let response = reqwest .post(format!("{}/v0/packages", config.api())) .multipart(reqwest::blocking::multipart::Form::new().part( "tarball", reqwest::blocking::multipart::Part::bytes(archive).file_name("package.tar.gz"), )) .send() - .context("failed to send request")? - .error_for_status() - .and_then(|response| response.text()) - { - Ok(response) => { - println!("{response}"); + .context("failed to send request")?; - Ok(()) - } - Err(e) - if e.status() - .is_some_and(|status| status == StatusCode::CONFLICT) => - { + let status = response.status(); + let text = response.text().context("failed to get response text")?; + match status { + StatusCode::CONFLICT => { println!("{}", "package version already exists".red().bold()); Ok(()) } - Err(e) - if e.status() - .is_some_and(|status| status == StatusCode::FORBIDDEN) => - { + StatusCode::FORBIDDEN => { println!( "{}", "unauthorized to publish under this scope".red().bold() @@ -441,7 +431,19 @@ impl PublishCommand { Ok(()) } - Err(e) => Err(e).context("failed to get response"), + StatusCode::BAD_REQUEST => { + println!("{}: {text}", "invalid package".red().bold()); + + Ok(()) + } + code if !code.is_success() => { + anyhow::bail!("failed to publish package: {code} ({text})"); + } + _ => { + println!("{text}"); + + Ok(()) + } } } }