-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement base data models for registry index
commit-id:1a33a4a3
- Loading branch information
Showing
9 changed files
with
291 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// FIXME(mkaput): Remove this when we will indeed use this. | ||
#[allow(unused)] | ||
pub mod models; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
use std::ops::Deref; | ||
use std::str::FromStr; | ||
|
||
use anyhow::ensure; | ||
use serde::{Deserialize, Serialize}; | ||
use url::Url; | ||
|
||
/// Wrapper over [`Url`] which ensures that the URL can be a joined, and has trailing slash. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
#[serde(try_from = "Url")] | ||
pub struct BaseUrl(Url); | ||
|
||
impl Deref for BaseUrl { | ||
type Target = Url; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
&self.0 | ||
} | ||
} | ||
|
||
impl TryFrom<Url> for BaseUrl { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(mut url: Url) -> Result<Self, Self::Error> { | ||
ensure!(!url.cannot_be_a_base(), "invalid base url: {url}"); | ||
|
||
if !url.path().ends_with('/') { | ||
url.set_path(&format!("{}/", url.path())); | ||
} | ||
|
||
Ok(Self(url)) | ||
} | ||
} | ||
|
||
impl FromStr for BaseUrl { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
BaseUrl::try_from(Url::parse(s)?) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::str::FromStr; | ||
use test_case::test_case; | ||
|
||
use super::BaseUrl; | ||
|
||
#[test] | ||
fn rejects_cannot_be_a_base_urls() { | ||
assert_eq!( | ||
"invalid base url: data:text/plain,Stuff", | ||
BaseUrl::from_str("data:text/plain,Stuff") | ||
.unwrap_err() | ||
.to_string(), | ||
); | ||
} | ||
|
||
#[test_case("https://example.com" => "https://example.com/")] | ||
#[test_case("https://example.com/file" => "https://example.com/file/")] | ||
#[test_case("https://example.com/path/" => "https://example.com/path/")] | ||
#[test_case("https://example.com/file.json" => "https://example.com/file.json/")] | ||
fn appends_trailing_slash_if_missing(url: &str) -> String { | ||
BaseUrl::from_str(url).unwrap().to_string() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use anyhow::{ensure, Result}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::registry::index::models::base_url::BaseUrl; | ||
use crate::registry::index::models::template_url::TemplateUrl; | ||
|
||
/// The `config.json` file stored in and defining the index. | ||
/// | ||
/// The config file may look like this: | ||
/// | ||
/// ```json | ||
/// { | ||
/// "version": 1, | ||
/// "api": "https://example.com/api/v1", | ||
/// "dl": "https://example.com/api/v1/download/{package}/{version}", | ||
/// "index": "https://example.com/index/{prefix}/{package}.json" | ||
/// } | ||
/// ``` | ||
/// | ||
/// ## URL Templates | ||
/// | ||
/// The values for the `"dl"` and `"index"` fields are URL templates. | ||
/// See documentation for [`TemplateUrl`] for supported expansion patterns. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
#[serde(rename_all = "kebab-case")] | ||
pub struct IndexConfig { | ||
/// Index version, must be `1` (numeric). | ||
pub version: IndexVersion, | ||
|
||
/// API endpoint for the registry. | ||
/// | ||
/// This is what's actually hit to perform operations like yanks, owner modifications, | ||
/// publish new packages, etc. | ||
/// If this is `None`, the registry does not support API commands. | ||
pub api: Option<BaseUrl>, | ||
|
||
/// Download endpoint for all packages. | ||
pub dl: TemplateUrl, | ||
|
||
/// Base URL for main index file files. | ||
/// | ||
/// Usually, this is a location where `config.json` lies, as the rest of index files resides | ||
/// alongside config. | ||
pub index: TemplateUrl, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] | ||
#[serde(into = "u8", try_from = "u8")] | ||
pub struct IndexVersion; | ||
|
||
impl TryFrom<u8> for IndexVersion { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(value: u8) -> Result<Self, Self::Error> { | ||
ensure!(value == 1, "unsupported index version: {value}"); | ||
Ok(Self) | ||
} | ||
} | ||
|
||
impl From<IndexVersion> for u8 { | ||
fn from(_: IndexVersion) -> Self { | ||
1 | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::IndexConfig; | ||
use crate::registry::index::models::TemplateUrl; | ||
|
||
#[test] | ||
fn deserialize() { | ||
let expected = IndexConfig { | ||
version: Default::default(), | ||
api: Some("https://example.com/api/v1/".parse().unwrap()), | ||
dl: TemplateUrl::new("https://example.com/api/v1/download/{package}/{version}"), | ||
index: TemplateUrl::new("https://example.com/index/{prefix}/{package}.json"), | ||
}; | ||
|
||
let actual: IndexConfig = serde_json::from_str( | ||
r#"{ | ||
"version": 1, | ||
"api": "https://example.com/api/v1", | ||
"dl": "https://example.com/api/v1/download/{package}/{version}", | ||
"index": "https://example.com/index/{prefix}/{package}.json" | ||
}"#, | ||
) | ||
.unwrap(); | ||
|
||
assert_eq!(actual, expected); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
pub use base_url::*; | ||
pub use config::*; | ||
pub use record::*; | ||
pub use template_url::*; | ||
|
||
mod base_url; | ||
mod config; | ||
mod record; | ||
mod template_url; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
use crate::core::PackageName; | ||
use semver::{Version, VersionReq}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
pub type IndexRecords = Vec<IndexRecord>; | ||
|
||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
pub struct IndexRecord { | ||
#[serde(rename = "v")] | ||
pub version: Version, | ||
#[serde(rename = "deps")] | ||
pub dependencies: IndexDependencies, | ||
#[serde(rename = "cksum")] | ||
pub checksum: String, | ||
} | ||
|
||
pub type IndexDependencies = Vec<IndexDependency>; | ||
|
||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
pub struct IndexDependency { | ||
pub name: PackageName, | ||
pub req: VersionReq, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
use std::fmt; | ||
|
||
use anyhow::{Context, Result}; | ||
use indoc::formatdoc; | ||
use serde::{Deserialize, Serialize}; | ||
use url::Url; | ||
|
||
use crate::core::PackageId; | ||
|
||
/// A template string which will generate the download or index URL for the specific package. | ||
/// | ||
/// The patterns `{package}` and `{version}` will be replaced with the package name and version | ||
/// (without leading `v`) respectively. The pattern `{prefix}` will be replaced with the package's | ||
/// prefix directory name (e.g. `ab/cd` for package named `abcd`). | ||
/// | ||
/// If the template contains no patterns, the template will expand to itself, which probably is | ||
/// not something you want. | ||
/// | ||
/// Upon expansion, the template must form a valid URL. | ||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
#[serde(transparent)] | ||
pub struct TemplateUrl(String); | ||
|
||
impl TemplateUrl { | ||
pub fn new(template: &str) -> Self { | ||
Self(template.to_owned()) | ||
} | ||
|
||
pub fn expand(&self, pkg: PackageId) -> Result<Url> { | ||
let package = pkg.name.as_str(); | ||
let version = pkg.version.to_string(); | ||
let prefix = pkg_prefix(package); | ||
|
||
let expansion = self | ||
.0 | ||
.replace("{package}", package) | ||
.replace("{version}", &version) | ||
.replace("{prefix}", &prefix); | ||
|
||
expansion.parse().with_context(|| { | ||
formatdoc! {r" | ||
failed to expand template url | ||
template: {self} | ||
expansion: {expansion} | ||
"} | ||
}) | ||
} | ||
} | ||
|
||
impl fmt::Display for TemplateUrl { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
fmt::Display::fmt(&self.0, f) | ||
} | ||
} | ||
|
||
/// Make a path to a package directory, which aligns to the index directory layout. | ||
fn pkg_prefix(name: &str) -> String { | ||
match name.len() { | ||
1 => "1".to_string(), | ||
2 => "2".to_string(), | ||
3 => format!("3/{}", &name[..1]), | ||
_ => format!("{}/{}", &name[0..2], &name[2..4]), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::core::PackageId; | ||
use crate::registry::index::models::TemplateUrl; | ||
use test_case::test_case; | ||
|
||
#[test] | ||
fn expand() { | ||
let template = TemplateUrl::new("https://example.com/{prefix}/{package}-{version}.json"); | ||
let package = PackageId::from_display_str("foobar v1.0.0").unwrap(); | ||
assert_eq!( | ||
"https://example.com/fo/ob/foobar-1.0.0.json", | ||
template.expand(package).unwrap().as_str() | ||
) | ||
} | ||
|
||
#[test_case("a" => "1")] | ||
#[test_case("ab" => "2")] | ||
#[test_case("abc" => "3/a")] | ||
#[test_case("Xyz" => "3/X")] | ||
#[test_case("AbCd" => "Ab/Cd")] | ||
#[test_case("pQrS" => "pQ/rS")] | ||
fn pkg_prefix(input: &str) -> String { | ||
super::pkg_prefix(input) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub mod index; | ||
|
||
pub const DEFAULT_REGISTRY_INDEX: &str = "https://index.there-is-no-registry-yet.dev/"; |