Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions crates/peryx-ecosystem-pypi/src/cache/page_stream/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl FreshJsonStream {
let preflight = match preflight {
JsonPreflight::Streaming {
body, transformer, raw, ..
} if !transformer.project_status_known() => match buffer_whole_page(body, raw, buffered_context).await {
} if !transformer.headers_known() => match buffer_whole_page(body, raw, buffered_context).await {
Ok((raw, served, summary)) => JsonPreflight::Complete { raw, served, summary },
Err(err) => {
release_flight(&self.state, &self.key, self.guard);
Expand Down Expand Up @@ -147,15 +147,15 @@ fn build_record(
}
}

/// Drain the rest of a page whose `files` preceded `meta`, then transform it whole with the project
/// Drain the rest of a page whose `files` preceded `project-status`, then transform it whole with the project
/// status seeded so a quarantined project withholds its files regardless of key order.
async fn buffer_whole_page(
mut body: futures_util::stream::BoxStream<'static, Result<Bytes, peryx_upstream::UpstreamError>>,
mut raw: Vec<u8>,
context: crate::stream::PageContext,
) -> Result<(Vec<u8>, Vec<u8>, PageSummary), CacheError> {
use futures_util::StreamExt as _;
// Bound the buffer as the transformer does: a `files`-before-`meta` page whose whole body would
// Bound the buffer as the transformer does: a `files`-before-status page whose whole body would
// pass `MAX_PAGE_BYTES` must fail here, before `raw` grows unbounded, rather than after the second
// transformer pass allocates a matching output vector from the oversized input.
while let Some(chunk) = body.next().await {
Expand All @@ -165,12 +165,9 @@ async fn buffer_whole_page(
}
raw.extend_from_slice(&chunk);
}
let status = crate::parse_detail(&raw)
.map_err(CacheError::Simple)?
.meta
.project_status;
let mut transformer = PageTransformer::new(context);
transformer.seed_project_status(status);
let parsed = crate::parse_detail(&raw).map_err(CacheError::Simple)?;
transformer.seed_project_status(parsed.meta.project_status, parsed.meta.project_status_reason);
let mut served = Vec::with_capacity(raw.len());
transformer.push_into(&raw, &mut served).map_err(transform_error)?;
let summary = transformer.finish().map_err(transform_error)?;
Expand Down Expand Up @@ -217,7 +214,7 @@ async fn preflight_json_stream(
transformer
.push_into(&chunk[position..=position], &mut outgoing)
.map_err(transform_error)?;
if transformer.meta_preflight_done() || raw.len() >= JSON_META_PREFLIGHT_BYTES {
if transformer.header_preflight_done() || raw.len() >= JSON_META_PREFLIGHT_BYTES {
pending.push_back(Bytes::copy_from_slice(&outgoing));
body = prepend_chunk(body, chunk.slice(position + 1..));
return Ok(JsonPreflight::Streaming {
Expand Down
2 changes: 1 addition & 1 deletion crates/peryx-ecosystem-pypi/src/cache/page_stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ fn transform_whole(
let mut transformer = PageTransformer::new(context);
// Seed the status so a quarantined page withholds its files whether `meta` precedes or follows
// `files`; the whole-page pass otherwise learns the status only once it reaches `meta`.
transformer.seed_project_status(detail.meta.project_status);
transformer.seed_project_status(detail.meta.project_status, detail.meta.project_status_reason);
let mut out = Vec::with_capacity(record.body.len());
transformer.push_into(&record.body, &mut out).map_err(transform_error)?;
transformer.finish().map_err(transform_error)?;
Expand Down
12 changes: 6 additions & 6 deletions crates/peryx-ecosystem-pypi/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,19 +462,19 @@ pub fn ui_project_from_detail(value: &serde_json::Value) -> crate::view::Project
}
}

/// The publish status the page flags, drawn from the PEP 792 `project-status` marker in the detail
/// document's `meta`. Active, absent, and unrecognized markers carry `None`, so the page flags only a
/// state that departs from serving the project as usual.
/// The publish status the page flags, drawn from the PEP 792 `project-status` object. Active, absent,
/// and unrecognized markers carry `None`, so the page flags only a state that departs from serving
/// the project as usual.
#[cfg(feature = "serving")]
fn project_status(value: &serde_json::Value) -> Option<Box<crate::view::ProjectStatusView>> {
let meta = &value["meta"];
let status = meta["project-status"]
let project_status = &value["project-status"];
let status = project_status["status"]
.as_str()
.and_then(crate::ProjectStatus::from_marker)
.filter(|status| *status != crate::ProjectStatus::Active)?;
Some(Box::new(crate::view::ProjectStatusView {
marker: status.marker().to_owned(),
reason: meta["project-status-reason"]
reason: project_status["reason"]
.as_str()
.filter(|reason| !reason.is_empty())
.map(str::to_owned),
Expand Down
3 changes: 2 additions & 1 deletion crates/peryx-ecosystem-pypi/src/openapi/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub(super) fn project_detail() -> OperationBuilder {
json_response(
"The project detail page",
json!({
"meta": {"api-version": "1.4", "project-status": "active"},
"meta": {"api-version": "1.4"},
"project-status": {"status": "active"},
"name": "peryxpkg",
"versions": ["1.0"],
"files": [{
Expand Down
43 changes: 40 additions & 3 deletions crates/peryx-ecosystem-pypi/src/simple/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ const PEP700_MINOR: u64 = 1;
pub struct Meta {
#[serde(rename = "api-version")]
pub api_version: &'static str,
#[serde(rename = "project-status", skip_serializing_if = "Option::is_none")]
#[serde(skip)]
pub project_status: Option<String>,
#[serde(rename = "project-status-reason", skip_serializing_if = "Option::is_none")]
#[serde(skip)]
pub project_status_reason: Option<String>,
}

Expand Down Expand Up @@ -60,6 +60,35 @@ impl Meta {
.and_then(ProjectStatus::from_marker)
.unwrap_or(ProjectStatus::Active)
}

pub(super) fn project_status_object(&self) -> Option<ProjectStatusObject<'_>> {
(self.project_status.is_some() || self.project_status_reason.is_some()).then_some(ProjectStatusObject {
status: self.project_status.as_deref(),
reason: self.project_status_reason.as_deref(),
})
}
}

#[derive(Serialize)]
pub struct ProjectStatusObject<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) status: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) reason: Option<&'a str>,
}

#[derive(Deserialize)]
pub(super) struct IncomingProjectStatus {
#[serde(default)]
status: Option<String>,
#[serde(default)]
reason: Option<String>,
}

impl IncomingProjectStatus {
pub(super) fn into_parts(self) -> (Option<String>, Option<String>) {
(self.status, self.reason)
}
}

#[derive(Default, Deserialize)]
Expand All @@ -80,6 +109,14 @@ impl IncomingMeta {
self.project_status_reason,
)
}

pub(super) fn into_detail_meta(self, project_status: Option<IncomingProjectStatus>) -> Result<Meta, SimpleError> {
let (project_status, project_status_reason) = project_status.map_or(
(self.project_status, self.project_status_reason),
IncomingProjectStatus::into_parts,
);
Meta::from_upstream(self.api_version.as_deref(), project_status, project_status_reason)
}
}

/// The version peryx advertises for an upstream that declared `version`: its own ceiling once the
Expand Down Expand Up @@ -108,7 +145,7 @@ fn served_version(version: Option<&str>) -> Result<&'static str, SimpleError> {
})
}

fn validate_project_status(status: &str) -> Result<(), SimpleError> {
pub(super) fn validate_project_status(status: &str) -> Result<(), SimpleError> {
ProjectStatus::from_marker(status)
.map(|_| ())
.ok_or_else(|| SimpleError::InvalidProjectStatus(status.to_owned()))
Expand Down
4 changes: 4 additions & 0 deletions crates/peryx-ecosystem-pypi/src/simple/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ mod render;

pub use error::SimpleError;
pub use file::{CoreMetadata, File, Provenance, Yanked};
#[cfg(feature = "serving")]
pub use meta::ProjectStatusObject;
pub use meta::{API_VERSION, API_VERSION_BASE, Meta, ProjectStatus};
#[cfg(feature = "serving")]
pub use parse::absolutize;
#[cfg(feature = "serving")]
pub use parse::parse_project_status;
pub use parse::{
DetailSink, ParsedDetail, ProjectDetail, ProjectList, ProjectListEntry, StreamedDetail, parse_detail, parse_index,
parse_meta, stream_detail_json, to_json,
Expand Down
46 changes: 42 additions & 4 deletions crates/peryx-ecosystem-pypi/src/simple/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use std::fmt;
use std::io::Read;

use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
use serde::ser::SerializeMap as _;
use serde::{Deserialize, Serialize};
use url::Url;

use super::meta::IncomingMeta;
use super::meta::{IncomingMeta, IncomingProjectStatus};
use super::{File, Meta, SimpleError};

/// Resolve `url` in place against `base`, turning a relative, root-relative, or protocol-relative
Expand Down Expand Up @@ -36,6 +37,8 @@ pub struct ParsedDetail {
#[derive(Deserialize)]
struct IncomingDetail {
meta: IncomingMeta,
#[serde(rename = "project-status", default, deserialize_with = "deserialize_project_status")]
project_status: Option<IncomingProjectStatus>,
name: String,
#[serde(default)]
versions: Vec<String>,
Expand All @@ -50,7 +53,7 @@ struct IncomingDetail {
pub fn parse_detail(bytes: &[u8]) -> Result<ParsedDetail, SimpleError> {
let detail: IncomingDetail = serde_json::from_slice(bytes)?;
Ok(ParsedDetail {
meta: detail.meta.into_meta()?,
meta: detail.meta.into_detail_meta(detail.project_status)?,
name: detail.name,
versions: detail.versions,
files: detail.files,
Expand Down Expand Up @@ -94,14 +97,15 @@ pub fn stream_detail_json<S: DetailSink>(
let header = DetailSeed { base, sink }.deserialize(&mut deserializer)?;
deserializer.end()?;
Ok(StreamedDetail {
meta: header.meta.into_meta()?,
meta: header.meta.into_detail_meta(header.project_status)?,
name: header.name,
versions: header.versions,
})
}

struct IncomingStreamedDetail {
meta: IncomingMeta,
project_status: Option<IncomingProjectStatus>,
name: String,
versions: Vec<String>,
}
Expand Down Expand Up @@ -136,12 +140,14 @@ impl<'de, S: DetailSink> Visitor<'de> for DetailVisitor<'_, S> {

fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut meta = None;
let mut project_status = None;
let mut name = None;
let mut versions = Vec::new();
let mut files = false;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"meta" => meta = Some(map.next_value()?),
"project-status" => project_status = Some(map.next_value()?),
"name" => name = Some(map.next_value()?),
"versions" => versions = map.next_value()?,
"files" => {
Expand All @@ -161,6 +167,7 @@ impl<'de, S: DetailSink> Visitor<'de> for DetailVisitor<'_, S> {
}
Ok(IncomingStreamedDetail {
meta: meta.ok_or_else(|| serde::de::Error::missing_field("meta"))?,
project_status,
name: name.ok_or_else(|| serde::de::Error::missing_field("name"))?,
versions,
})
Expand Down Expand Up @@ -214,6 +221,22 @@ pub fn parse_meta(bytes: &[u8]) -> Result<Meta, SimpleError> {
meta.into_meta()
}

#[cfg(feature = "serving")]
pub fn parse_project_status(bytes: &[u8]) -> Result<(Option<String>, Option<String>), SimpleError> {
let status: IncomingProjectStatus = serde_json::from_slice(bytes)?;
let (status, reason) = status.into_parts();
if let Some(status) = status.as_deref() {
super::meta::validate_project_status(status)?;
}
Ok((status, reason))
}

fn deserialize_project_status<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<IncomingProjectStatus>, D::Error> {
IncomingProjectStatus::deserialize(deserializer).map(Some)
}

#[derive(Deserialize)]
struct IncomingProjectListEntry {
name: String,
Expand Down Expand Up @@ -245,14 +268,29 @@ pub fn parse_index(bytes: &[u8]) -> Result<ProjectList, SimpleError> {
}

/// A project's detail response (`/simple/<project>/`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectDetail {
pub meta: Meta,
pub name: String,
pub versions: Vec<String>,
pub files: Vec<File>,
}

impl Serialize for ProjectDetail {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let project_status = self.meta.project_status_object();
let mut map = serializer.serialize_map(Some(4 + usize::from(project_status.is_some())))?;
map.serialize_entry("meta", &self.meta)?;
if let Some(project_status) = project_status {
map.serialize_entry("project-status", &project_status)?;
}
map.serialize_entry("name", &self.name)?;
map.serialize_entry("versions", &self.versions)?;
map.serialize_entry("files", &self.files)?;
map.end()
}
}

/// One entry in the root project list.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProjectListEntry {
Expand Down
Loading