use semver::Version;
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use std::collections::HashMap;
use std::hash::BuildHasher;
use std::rc::Rc;
pub trait Files {
fn file(&self, path: &str) -> Option<String>;
}
pub trait Packs {
fn releases(&self, pack: &str) -> Vec<Release>;
fn pack_file(&self, pack: &str, version: &Version, path: &str) -> Option<String>;
}
pub trait Fetch {
fn fetch(&self, url: &str) -> Option<Fetched>;
}
pub trait Data {
fn data(&self, source: &str) -> Option<Json>;
}
pub trait Time {
fn offset(&self, zone: &str, at: i64) -> Option<i32>;
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Release {
pub version: Version,
#[serde(default)]
pub integrity: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct LoadedRelease {
#[serde(flatten)]
pub release: Release,
pub files: HashMap<String, String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Needed {
pub remote: Vec<String>,
pub host: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Fetched {
pub text: String,
pub content_type: Option<String>,
}
impl Files for () {
fn file(&self, _path: &str) -> Option<String> {
None
}
}
impl<S: BuildHasher> Files for HashMap<String, String, S> {
fn file(&self, path: &str) -> Option<String> {
self.get(path).cloned()
}
}
impl Packs for () {
fn releases(&self, _pack: &str) -> Vec<Release> {
Vec::new()
}
fn pack_file(&self, _pack: &str, _version: &Version, _path: &str) -> Option<String> {
None
}
}
impl<S: BuildHasher> Packs for HashMap<String, Vec<LoadedRelease>, S> {
fn releases(&self, pack: &str) -> Vec<Release> {
self.get(pack)
.into_iter()
.flatten()
.map(|loaded| loaded.release.clone())
.collect()
}
fn pack_file(&self, pack: &str, version: &Version, path: &str) -> Option<String> {
self.get(pack)?
.iter()
.find(|loaded| loaded.release.version == *version)?
.files
.get(path)
.cloned()
}
}
impl Fetch for () {
fn fetch(&self, _url: &str) -> Option<Fetched> {
None
}
}
impl<S: BuildHasher> Fetch for HashMap<String, Fetched, S> {
fn fetch(&self, url: &str) -> Option<Fetched> {
self.get(url).cloned()
}
}
impl Data for () {
fn data(&self, _source: &str) -> Option<Json> {
None
}
}
impl<S: BuildHasher> Data for HashMap<String, Json, S> {
fn data(&self, source: &str) -> Option<Json> {
self.get(source).cloned()
}
}
impl<T: Files + ?Sized> Files for Rc<T> {
fn file(&self, path: &str) -> Option<String> {
(**self).file(path)
}
}
impl<T: Packs + ?Sized> Packs for Rc<T> {
fn releases(&self, pack: &str) -> Vec<Release> {
(**self).releases(pack)
}
fn pack_file(&self, pack: &str, version: &Version, path: &str) -> Option<String> {
(**self).pack_file(pack, version, path)
}
}
impl<T: Fetch + ?Sized> Fetch for Rc<T> {
fn fetch(&self, url: &str) -> Option<Fetched> {
(**self).fetch(url)
}
}
impl<T: Data + ?Sized> Data for Rc<T> {
fn data(&self, source: &str) -> Option<Json> {
(**self).data(source)
}
}
impl Time for () {
fn offset(&self, zone: &str, _at: i64) -> Option<i32> {
(zone == "UTC").then_some(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_unit_host_has_nothing_but_utc() {
assert_eq!(().file("a.comp.md"), None);
assert_eq!(().releases("@acme/kit"), Vec::new());
assert_eq!(().pack_file("@acme/kit", &Version::new(1, 0, 0), "a"), None);
assert_eq!(().fetch("https://a.org/x.json"), None);
assert_eq!(().data("tasks"), None);
assert_eq!(().offset("UTC", 0), Some(0));
assert_eq!(().offset("Europe/Berlin", 0), None);
}
#[test]
fn packs_in_memory_serve_their_releases() {
let packs: HashMap<String, Vec<LoadedRelease>> = serde_json::from_value(json!({
"@acme/charts": [
{ "version": "1.4.2", "integrity": "sha512-x", "files": { "package.json": "{}" } },
{ "version": "1.5.0", "files": {} }
]
}))
.expect("packs");
let releases = packs.releases("@acme/charts");
assert_eq!(releases.len(), 2);
assert_eq!(releases[0].integrity.as_deref(), Some("sha512-x"));
let version = Version::new(1, 4, 2);
assert_eq!(
packs.pack_file("@acme/charts", &version, "package.json"),
Some("{}".to_string())
);
assert_eq!(
packs.pack_file("@acme/charts", &Version::new(2, 0, 0), "a"),
None
);
assert_eq!(packs.releases("@acme/maps"), Vec::new());
}
#[test]
fn data_in_memory_is_served_by_url_and_source() {
let fetched = Fetched {
text: "{}".to_string(),
content_type: Some("application/json".to_string()),
};
let remote = HashMap::from([("https://a.org/x.json".to_string(), fetched.clone())]);
assert_eq!(remote.fetch("https://a.org/x.json"), Some(fetched));
assert_eq!(remote.fetch("https://a.org/y.json"), None);
let host = HashMap::from([("tasks".to_string(), json!([1]))]);
assert_eq!(host.data("tasks"), Some(json!([1])));
assert_eq!(host.data("notes"), None);
}
#[test]
fn a_shared_host_serves_what_it_holds() {
let files = Rc::new(HashMap::from([("a".to_string(), "A".to_string())]));
assert_eq!(files.file("a"), Some("A".to_string()));
let none = Rc::new(());
assert_eq!(none.releases("@acme/kit"), Vec::new());
assert_eq!(
none.pack_file("@acme/kit", &Version::new(1, 0, 0), "a"),
None
);
assert_eq!(none.fetch("https://a.org/x.json"), None);
assert_eq!(none.data("tasks"), None);
}
}