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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
- `/src/tests/` - Backend integration tests with snapshot testing using `insta`
- `/src/config/` - Configuration loading and validation
- `/src/util/` - Shared utilities (errors, authentication, pagination)
- `/src/views.rs` - Response serialization
- `/app/` - Frontend Ember.js application
- `/app/components/` - Reusable UI components (80+ components with scoped CSS files)
- `/app/routes/` and `/app/controllers/` - Route handlers and data loading
Expand All @@ -19,6 +18,7 @@
- `/app/adapters/`, `/app/serializers/` - Ember Data adapter layer
- `/app/services/` - Shared services (session, notifications, API client)
- `/crates/` - Workspace crates providing specialized functionality
- `crates_io_api_types/` - API response serialization types
- `crates_io_database/` - Database models and schema (Diesel ORM)
- `crates_io_worker/` - Background job queue system
- `crates_io_index/` - Git index management for crate metadata
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ bigdecimal = { version = "=0.4.8", features = ["serde"] }
bon = "=3.8.1"
cargo-manifest = "=0.19.1"
colored = "=3.0.0"
crates_io_api_types = { path = "crates/crates_io_api_types" }
crates_io_cdn_logs = { path = "crates/crates_io_cdn_logs" }
crates_io_database = { path = "crates/crates_io_database" }
crates_io_database_dump = { path = "crates/crates_io_database_dump" }
Expand Down
24 changes: 24 additions & 0 deletions crates/crates_io_api_types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "crates_io_api_types"
version = "0.0.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/rust-lang/crates.io"
description = "API response types for crates.io"
edition = "2024"

[lints]
workspace = true

[dependencies]
chrono = { version = "=0.4.42", default-features = false, features = ["serde"] }
crates_io_database = { path = "../crates_io_database" }
indexmap = { version = "=2.11.4", features = ["serde"] }
semver = { version = "=1.0.27", features = ["serde"] }
sentry-core = "=0.45.0"
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = "=1.0.145"
url = "=2.5.7"
utoipa = { version = "=5.4.0", features = ["chrono"] }

[dev-dependencies]
claims = "=0.8.0"
12 changes: 12 additions & 0 deletions crates/crates_io_api_types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# crates_io_api_types

This crate contains some of the shared API response and request types used by the crates.io API.

These types are serialized to/from JSON and represent the public API surface that clients interact with. They are distinct from the database models in `crates_io_database`, which represent the internal database schema.

The crate includes types for publishing crates, trusted publishing configuration, release tracking metadata, and various encodable domain objects (crates, versions, users, teams, categories, keywords).

## Design principles

- **No business logic**: Types are primarily data structures with minimal behavior beyond serialization
- **OpenAPI schema**: Types include `utoipa::ToSchema` derives for automatic OpenAPI documentation generation
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
//! to and from structs. The serializing is only utilised in
//! integration tests.

use crates_io_database::models::DependencyKind;
use serde::{Deserialize, Serialize};

use crate::models::DependencyKind;

#[derive(Deserialize, Serialize, Debug)]
pub struct PublishMetadata {
pub name: String,
Expand Down
18 changes: 11 additions & 7 deletions src/views.rs → crates/crates_io_api_types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use crate::external_urls::remove_blocked_urls;
use crate::models::{
mod external_urls;
pub mod krate_publish;
pub mod release_tracks;
pub mod trustpub;

pub use self::external_urls::remove_blocked_urls;
pub use self::krate_publish::{EncodableCrateDependency, PublishMetadata};

use chrono::{DateTime, Utc};
use crates_io_database::models::{
ApiToken, Category, Crate, Dependency, DependencyKind, Keyword, Owner, ReverseDependency, Team,
TopVersions, TrustpubData, User, Version, VersionDownload, VersionOwnerAction,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

pub mod krate_publish;
pub use self::krate_publish::{EncodableCrateDependency, PublishMetadata};

#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)]
#[schema(as = Category)]
pub struct EncodableCategory {
Expand Down Expand Up @@ -397,7 +401,7 @@ impl EncodableCrate {
let default_version = default_version.map(ToString::to_string);
if default_version.is_none() {
let message = format!("Crate `{name}` has no default version");
sentry::capture_message(&message, sentry::Level::Info);
sentry_core::capture_message(&message, sentry_core::Level::Info);
}
let yanked = yanked.unwrap_or_default();

Expand Down
177 changes: 177 additions & 0 deletions crates/crates_io_api_types/src/release_tracks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use indexmap::IndexMap;
use serde::Serialize;

#[derive(Debug, Eq, PartialEq, Serialize)]
pub struct ReleaseTracks(IndexMap<ReleaseTrackName, ReleaseTrackDetails>);

impl ReleaseTracks {
// Return the release tracks based on a sorted semver versions iterator (in descending order).
// **Remember to** filter out yanked versions manually before calling this function.
pub fn from_sorted_semver_iter<'a, I>(versions: I) -> Self
where
I: Iterator<Item = &'a semver::Version>,
{
let mut map = IndexMap::new();
for num in versions.filter(|num| num.pre.is_empty()) {
let key = ReleaseTrackName::from_semver(num);
let prev = map.last();
if prev.filter(|&(k, _)| *k == key).is_none() {
map.insert(
key,
ReleaseTrackDetails {
highest: num.clone(),
},
);
}
}

Self(map)
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ReleaseTrackName {
Minor(u64),
Major(u64),
}

impl ReleaseTrackName {
pub fn from_semver(version: &semver::Version) -> Self {
if version.major == 0 {
Self::Minor(version.minor)
} else {
Self::Major(version.major)
}
}
}

impl std::fmt::Display for ReleaseTrackName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Minor(minor) => write!(f, "0.{minor}"),
Self::Major(major) => write!(f, "{major}"),
}
}
}

impl serde::Serialize for ReleaseTrackName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
Self: std::fmt::Display,
{
serializer.collect_str(self)
}
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
pub struct ReleaseTrackDetails {
pub highest: semver::Version,
}

#[cfg(test)]
mod tests {
use super::{ReleaseTrackDetails, ReleaseTrackName, ReleaseTracks};
use indexmap::IndexMap;
use serde_json::json;

#[track_caller]
fn version(str: &str) -> semver::Version {
semver::Version::parse(str).unwrap()
}

#[test]
fn release_tracks_empty() {
let versions = [];
assert_eq!(
ReleaseTracks::from_sorted_semver_iter(versions.into_iter()),
ReleaseTracks(IndexMap::new())
);
}

#[test]
fn release_tracks_prerelease() {
let versions = [version("1.0.0-beta.5")];
assert_eq!(
ReleaseTracks::from_sorted_semver_iter(versions.iter()),
ReleaseTracks(IndexMap::new())
);
}

#[test]
fn release_tracks_multiple() {
let versions = [
"100.1.1",
"100.1.0",
"1.3.5",
"1.2.5",
"1.1.5",
"0.4.0-rc.1",
"0.3.23",
"0.3.22",
"0.3.21-pre.0",
"0.3.20",
"0.3.3",
"0.3.2",
"0.3.1",
"0.3.0",
"0.2.1",
"0.2.0",
"0.1.2",
"0.1.1",
]
.map(version);

let release_tracks = ReleaseTracks::from_sorted_semver_iter(versions.iter());
assert_eq!(
release_tracks,
ReleaseTracks(IndexMap::from([
(
ReleaseTrackName::Major(100),
ReleaseTrackDetails {
highest: version("100.1.1")
}
),
(
ReleaseTrackName::Major(1),
ReleaseTrackDetails {
highest: version("1.3.5")
}
),
(
ReleaseTrackName::Minor(3),
ReleaseTrackDetails {
highest: version("0.3.23")
}
),
(
ReleaseTrackName::Minor(2),
ReleaseTrackDetails {
highest: version("0.2.1")
}
),
(
ReleaseTrackName::Minor(1),
ReleaseTrackDetails {
highest: version("0.1.2")
}
),
]))
);

let json = serde_json::from_str::<serde_json::Value>(
&serde_json::to_string(&release_tracks).unwrap(),
)
.unwrap();
assert_eq!(
json,
json!({
"100": { "highest": "100.1.1" },
"1": { "highest": "1.3.5" },
"0.3": { "highest": "0.3.23" },
"0.2": { "highest": "0.2.1" },
"0.1": { "highest": "0.1.2" }
})
);
}
}
39 changes: 39 additions & 0 deletions crates/crates_io_api_types/src/trustpub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, utoipa::ToSchema)]
#[schema(as = GitHubConfig)]
pub struct GitHubConfig {
#[schema(example = 42)]
pub id: i32,
#[schema(example = "regex")]
#[serde(rename = "crate")]
pub krate: String,
#[schema(example = "rust-lang")]
pub repository_owner: String,
#[schema(example = 5430905)]
pub repository_owner_id: i32,
#[schema(example = "regex")]
pub repository_name: String,
#[schema(example = "ci.yml")]
pub workflow_filename: String,
#[schema(example = json!(null))]
pub environment: Option<String>,
pub created_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize, utoipa::ToSchema)]
#[schema(as = NewGitHubConfig)]
pub struct NewGitHubConfig {
#[schema(example = "regex")]
#[serde(rename = "crate")]
pub krate: String,
#[schema(example = "rust-lang")]
pub repository_owner: String,
#[schema(example = "regex")]
pub repository_name: String,
#[schema(example = "ci.yml")]
pub workflow_filename: String,
#[schema(example = json!(null))]
pub environment: Option<String>,
}
Loading
Loading