Skip to content

Commit

Permalink
Implement base data models for registry index
Browse files Browse the repository at this point in the history
commit-id:1a33a4a3
  • Loading branch information
mkaput committed Oct 9, 2023
1 parent c2e1b1d commit ebfd60f
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 1 deletion.
3 changes: 2 additions & 1 deletion scarb/src/core/source/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::core::source::Source;
use crate::core::Config;
use crate::internal::fsx::PathBufUtf8Ext;
use crate::internal::static_hash_cache::StaticHashCache;
use crate::registry::DEFAULT_REGISTRY_INDEX;
use crate::sources::canonical_url::CanonicalUrl;

/// Unique identifier for a source of packages.
Expand Down Expand Up @@ -152,7 +153,7 @@ impl SourceId {

pub fn default_registry() -> Self {
static CACHE: Lazy<SourceId> = Lazy::new(|| {
let url = Url::parse("https://there-is-no-default-registry-yet.com").unwrap();
let url = Url::parse(DEFAULT_REGISTRY_INDEX).unwrap();
SourceId::new(url, SourceKind::Registry).unwrap()
});
*CACHE
Expand Down
1 change: 1 addition & 0 deletions scarb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod internal;
pub mod manifest_editor;
pub mod ops;
pub mod process;
mod registry;
mod resolver;
mod sources;
mod subcommands;
Expand Down
3 changes: 3 additions & 0 deletions scarb/src/registry/index/mod.rs
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;
67 changes: 67 additions & 0 deletions scarb/src/registry/index/models/base_url.rs
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()
}
}
92 changes: 92 additions & 0 deletions scarb/src/registry/index/models/config.rs
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);
}
}
9 changes: 9 additions & 0 deletions scarb/src/registry/index/models/mod.rs
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;
23 changes: 23 additions & 0 deletions scarb/src/registry/index/models/record.rs
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,
}
91 changes: 91 additions & 0 deletions scarb/src/registry/index/models/template_url.rs
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)
}
}
3 changes: 3 additions & 0 deletions scarb/src/registry/mod.rs
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/";

0 comments on commit ebfd60f

Please sign in to comment.