use crate::{
AuthorityId, DependencySelector, PackageError, WebId, fail, validate_logical_name,
validate_source_path, validate_stable,
};
use kcode_k1_transaction_id::TxId;
use semver::Version;
use serde::Deserialize;
use std::str::FromStr;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Manifest {
name: String,
version: String,
entry: String,
tests: String,
dependencies: Vec<ManifestDependency>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ManifestDependency {
authority: String,
name: String,
selector: String,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SourceFile(String, Vec<u8>);
impl SourceFile {
pub fn new(path: impl Into<String>, bytes: Vec<u8>) -> Self {
Self(path.into(), bytes)
}
pub fn path(&self) -> &str {
&self.0
}
pub fn bytes(&self) -> &[u8] {
&self.1
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WebDependency(AuthorityId, String, DependencySelector);
impl WebDependency {
pub const fn authority(&self) -> AuthorityId {
self.0
}
pub fn name(&self) -> &str {
&self.1
}
pub fn selector(&self) -> &DependencySelector {
&self.2
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct SourcePackage {
id: WebId,
files: Vec<SourceFile>,
entry: String,
tests: String,
dependencies: Vec<WebDependency>,
}
impl SourcePackage {
pub fn new(id: WebId, mut files: Vec<SourceFile>) -> Result<Self, PackageError> {
files
.iter()
.try_for_each(|file| validate_source_path(file.path()))?;
files.sort();
if files
.windows(2)
.any(|pair| pair[0].path() == pair[1].path())
{
return fail("duplicate source path");
}
if files
.iter()
.any(|file| has_file_ancestor(&files, file.path()))
{
return fail("source path collides with a file ancestor");
}
let manifest_source = required_utf8(&files, "k1-web.json")?;
required_utf8(&files, "Documentation.md")?;
let manifest: Manifest = serde_json::from_str(manifest_source)
.map_err(|cause| PackageError(format!("invalid k1-web.json: {cause}")))?;
validate_identity(&id, &manifest)?;
required_javascript(&files, &manifest.entry, "entry")?;
required_javascript(&files, &manifest.tests, "tests")?;
let dependencies = parse_dependencies(manifest.dependencies)?;
Ok(Self {
id,
files,
entry: manifest.entry,
tests: manifest.tests,
dependencies,
})
}
pub fn id(&self) -> &WebId {
&self.id
}
pub fn files(&self) -> &[SourceFile] {
&self.files
}
pub fn entry(&self) -> &str {
&self.entry
}
pub fn tests(&self) -> &str {
&self.tests
}
pub fn dependencies(&self) -> &[WebDependency] {
&self.dependencies
}
}
pub(crate) fn validate_manifest_for_projection(source: &str) -> Result<(), PackageError> {
let manifest: Manifest = serde_json::from_str(source)
.map_err(|cause| PackageError(format!("invalid k1-web.json: {cause}")))?;
validate_logical_name(&manifest.name)?;
canonical_manifest_version(&manifest.version)?;
validate_javascript_path(&manifest.entry, "entry")?;
validate_javascript_path(&manifest.tests, "tests")?;
parse_dependencies(manifest.dependencies)?;
Ok(())
}
fn has_file_ancestor(files: &[SourceFile], path: &str) -> bool {
path.match_indices('/').any(|(index, _)| {
files
.binary_search_by(|file| file.path().cmp(&path[..index]))
.is_ok()
})
}
fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
let file = files
.iter()
.find(|file| file.path() == path)
.ok_or_else(|| PackageError(format!("missing {path}")))?;
std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
}
fn required_javascript(files: &[SourceFile], path: &str, field: &str) -> Result<(), PackageError> {
validate_javascript_path(path, field)?;
required_utf8(files, path)?;
Ok(())
}
fn validate_javascript_path(path: &str, field: &str) -> Result<(), PackageError> {
validate_source_path(path)?;
if !(path.ends_with(".js") || path.ends_with(".mjs")) {
return fail(format!("manifest {field} must name a JavaScript file"));
}
Ok(())
}
fn validate_identity(id: &WebId, manifest: &Manifest) -> Result<(), PackageError> {
if manifest.name != id.family().logical_name() {
return fail("manifest name does not match package identity");
}
let version = canonical_manifest_version(&manifest.version)?;
if &version != id.version() {
return fail("manifest version does not match package identity canonically");
}
Ok(())
}
fn canonical_manifest_version(text: &str) -> Result<Version, PackageError> {
let version = Version::parse(text)
.map_err(|_| PackageError("manifest version is not valid SemVer".into()))?;
validate_stable(&version)?;
if version.to_string() != text {
return fail("manifest version does not use canonical spelling");
}
Ok(version)
}
fn parse_dependencies(
declarations: Vec<ManifestDependency>,
) -> Result<Vec<WebDependency>, PackageError> {
let mut dependencies = Vec::with_capacity(declarations.len());
for declaration in declarations {
validate_logical_name(&declaration.name)?;
dependencies.push(WebDependency(
parse_authority(&declaration.authority)?,
declaration.name,
DependencySelector::parse(&declaration.selector)?,
));
}
dependencies.sort();
if dependencies.windows(2).any(|pair| pair[0] == pair[1]) {
return fail("duplicate dependency declaration");
}
Ok(dependencies)
}
fn parse_authority(text: &str) -> Result<AuthorityId, PackageError> {
let transaction_id =
TxId::from_str(text).map_err(|_| PackageError("invalid dependency authority".into()))?;
if transaction_id.to_string() != text {
return fail("dependency authority must use canonical spelling");
}
Ok(AuthorityId::new(transaction_id))
}