pub mod async_util;
#[cfg(feature = "caching")]
pub mod cache;
#[cfg(feature = "client")]
pub mod client;
mod id;
pub mod search;
#[cfg(feature = "server")]
pub mod server;
#[cfg(feature = "client")]
pub mod standalone;
pub mod storage;
#[cfg(feature = "test-tools")]
pub mod testing;
pub mod filters;
#[doc(inline)]
pub use id::Id;
#[doc(inline)]
pub use search::Matches;
use semver::{Compat, Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::hash::Hash;
use search::SearchOptions;
pub const BINDLE_VERSION_1: &str = "1.0.0";
pub type FeatureMap = BTreeMap<String, BTreeMap<String, String>>;
pub type AnnotationMap = BTreeMap<String, String>;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Invoice {
pub bindle_version: String,
pub yanked: Option<bool>,
pub bindle: BindleSpec,
pub annotations: Option<BTreeMap<String, String>>,
pub parcel: Option<Vec<Parcel>>,
pub group: Option<Vec<Group>>,
}
impl Invoice {
pub fn name(&self) -> String {
format!("{}/{}", self.bindle.id.name(), self.bindle.id.version())
}
pub fn canonical_name(&self) -> String {
self.bindle.id.sha()
}
fn version_in_range(&self, requirement: &str) -> bool {
version_compare(self.bindle.id.version(), requirement)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BindleSpec {
#[serde(flatten)]
pub id: Id,
pub description: Option<String>,
pub authors: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Parcel {
pub label: Label,
pub conditions: Option<Condition>,
}
impl Parcel {
pub fn member_of(&self, group: &str) -> bool {
match &self.conditions {
Some(conditions) => match &conditions.member_of {
Some(groups) => groups.iter().any(|g| *g == group),
None => false,
},
None => false,
}
}
pub fn is_global_group(&self) -> bool {
match &self.conditions {
Some(conditions) => match &conditions.member_of {
Some(groups) => groups.is_empty(),
None => true,
},
None => true,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Label {
pub sha256: String,
pub media_type: String,
pub name: String,
pub size: u64,
pub annotations: Option<AnnotationMap>,
pub feature: Option<FeatureMap>,
}
impl Label {
pub fn new(name: String, sha256: String) -> Self {
Label {
name,
sha256,
..Label::default()
}
}
}
impl Default for Label {
fn default() -> Self {
Self {
sha256: "".to_owned(),
media_type: "application/octet-stream".to_owned(),
name: "".to_owned(),
size: 0,
annotations: None,
feature: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Condition {
pub member_of: Option<Vec<String>>,
pub requires: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Group {
pub name: String,
pub required: Option<bool>,
pub satisfied_by: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct InvoiceCreateResponse {
pub invoice: Invoice,
pub missing: Option<Vec<Label>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct MissingParcelsResponse {
pub missing: Vec<Label>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
error: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct QueryOptions {
#[serde(alias = "q")]
pub query: Option<String>,
#[serde(alias = "v")]
pub version: Option<String>,
#[serde(alias = "o")]
pub offset: Option<u64>,
#[serde(alias = "l")]
pub limit: Option<u8>,
pub strict: Option<bool>,
pub yanked: Option<bool>,
}
impl From<QueryOptions> for SearchOptions {
fn from(qo: QueryOptions) -> Self {
let defaults = SearchOptions::default();
SearchOptions {
limit: qo.limit.unwrap_or(defaults.limit),
offset: qo.offset.unwrap_or(defaults.offset),
strict: qo.strict.unwrap_or(defaults.strict),
yanked: qo.yanked.unwrap_or(defaults.yanked),
}
}
}
fn version_compare(version: &Version, requirement: &str) -> bool {
if requirement.is_empty() {
return true;
}
match VersionReq::parse_compat(requirement, Compat::Npm) {
Ok(req) => {
return req.matches(version);
}
Err(e) => {
log::error!("SemVer range could not parse: {}", e);
}
}
false
}
#[cfg(test)]
mod test {
use super::*;
use std::fs::read_to_string;
use std::path::Path;
#[test]
fn test_invoice_should_serialize() {
let label = Label {
sha256: "abcdef1234567890987654321".to_owned(),
media_type: "text/toml".to_owned(),
name: "foo.toml".to_owned(),
size: 101,
annotations: None,
feature: None,
};
let parcel = Parcel {
label,
conditions: None,
};
let parcels = Some(vec![parcel]);
let inv = Invoice {
bindle_version: BINDLE_VERSION_1.to_owned(),
yanked: None,
annotations: None,
bindle: BindleSpec {
id: "foo/1.2.3".parse().unwrap(),
description: Some("bar".to_owned()),
authors: Some(vec!["m butcher".to_owned()]),
},
parcel: parcels,
group: None,
};
let res = toml::to_string(&inv).unwrap();
let inv2 = toml::from_str::<Invoice>(res.as_str()).unwrap();
let b = inv2.bindle;
assert_eq!(b.id.name(), "foo".to_owned());
assert_eq!(b.id.version_string(), "1.2.3");
assert_eq!(b.description.unwrap().as_str(), "bar");
assert_eq!(b.authors.unwrap()[0], "m butcher".to_owned());
let parcels = inv2.parcel.unwrap();
assert_eq!(parcels.len(), 1);
let par = &parcels[0];
let lab = &par.label;
assert_eq!(lab.name, "foo.toml".to_owned());
assert_eq!(lab.media_type, "text/toml".to_owned());
assert_eq!(lab.sha256, "abcdef1234567890987654321".to_owned());
assert_eq!(lab.size, 101);
}
#[test]
fn test_examples_in_spec_parse() {
let test_files = vec![
"test/data/simple-invoice.toml",
"test/data/full-invoice.toml",
"test/data/alt-format-invoice.toml",
];
test_files.iter().for_each(|file| test_parsing_a_file(file));
}
fn test_parsing_a_file(filename: &str) {
let invoice_path = Path::new(filename);
let raw = read_to_string(invoice_path).expect("read file contents");
let invoice = toml::from_str::<Invoice>(raw.as_str()).expect("clean parse of invoice");
let _raw2 = toml::to_string_pretty(&invoice).expect("clean serialization of TOML");
}
#[test]
fn test_version_comparisons() {
let reqs = vec!["= 1.2.3", "1.2.3", "1.2.3", "^1.1", "~1.2", ""];
let version = Version::parse("1.2.3").unwrap();
reqs.iter().for_each(|r| {
if !version_compare(&version, r) {
panic!("Should have passed: {}", r)
}
});
let reqs = vec!["2", "%^&%^&%"];
reqs.iter()
.for_each(|r| assert!(!version_compare(&version, r)));
}
#[test]
fn parcel_no_groups() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
version = "1.2.3"
[[group]]
name = "images"
[[parcel]]
[parcel.label]
sha256 = "aaabbbcccdddeeefff"
name = "telescope.gif"
mediaType = "image/gif"
size = 123_456
[parcel.conditions]
memberOf = ["telescopes"]
[[parcel]]
[parcel.label]
sha256 = "111aaabbbcccdddeee"
name = "telescope.txt"
mediaType = "text/plain"
size = 123_456
"#;
let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
let parcels = invoice.parcel.expect("expected some parcels");
let img = &parcels[0];
let txt = &parcels[1];
assert!(img.member_of("telescopes"));
assert!(!img.is_global_group());
assert!(txt.is_global_group());
assert!(!txt.member_of("telescopes"));
}
}