mod core;
pub mod eval;
pub mod reg;
pub mod repo;
use crate::io::files::paths::Paths;
use crate::net::download;
use mcvm_pkg::declarative::{deserialize_declarative_package, DeclarativePackage};
use mcvm_pkg::PackageContentType;
use mcvm_shared::later::Later;
use std::fs;
use std::path::PathBuf;
use self::core::get_core_package;
use anyhow::{anyhow, bail, Context};
use mcvm_parse::parse::{lex_and_parse, Parsed};
use mcvm_pkg::metadata::{eval_metadata, PackageMetadata};
use mcvm_pkg::properties::{eval_properties, PackageProperties};
use mcvm_shared::pkg::PackageID;
use reqwest::Client;
const PKG_EXTENSION: &str = ".pkg.txt";
#[derive(Debug)]
pub struct Package {
pub id: PackageID,
pub location: PkgLocation,
pub content_type: PackageContentType,
pub data: Later<PkgData>,
}
#[derive(Debug, Clone)]
pub enum PkgLocation {
Local(PathBuf),
Remote(Option<String>),
Core,
}
#[derive(Debug)]
pub struct PkgData {
text: String,
contents: Later<PkgContents>,
metadata: Later<PackageMetadata>,
properties: Later<PackageProperties>,
}
impl PkgData {
pub fn new(text: &str) -> Self {
Self {
text: text.to_owned(),
contents: Later::new(),
metadata: Later::new(),
properties: Later::new(),
}
}
pub fn get_text(&self) -> String {
self.text.clone()
}
}
#[derive(Debug)]
pub enum PkgContents {
Script(Parsed),
Declarative(Box<DeclarativePackage>),
}
impl PkgContents {
pub fn get_script_contents(&self) -> &Parsed {
if let Self::Script(parsed) = &self {
parsed
} else {
panic!("Attempted to get script package contents from a non-script package");
}
}
pub fn get_declarative_contents(&self) -> &DeclarativePackage {
if let Self::Declarative(contents) = &self {
contents
} else {
panic!("Attempted to get declarative package contents from a non-declarative package");
}
}
}
impl Package {
pub fn new(id: PackageID, location: PkgLocation, content_type: PackageContentType) -> Self {
Self {
id,
location,
data: Later::new(),
content_type,
}
}
pub fn filename(&self) -> String {
format!("{}{PKG_EXTENSION}", self.id)
}
pub fn cached_path(&self, paths: &Paths) -> PathBuf {
let cache_dir = paths.project.cache_dir().join("pkg");
cache_dir.join(self.filename())
}
pub fn remove_cached(&self, paths: &Paths) -> anyhow::Result<()> {
let path = self.cached_path(paths);
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
pub async fn ensure_loaded(
&mut self,
paths: &Paths,
force: bool,
client: &Client,
) -> anyhow::Result<()> {
if self.data.is_empty() {
match &self.location {
PkgLocation::Local(path) => {
if !path.exists() {
bail!("Local package path does not exist");
}
self.data
.fill(PkgData::new(&tokio::fs::read_to_string(path).await?));
}
PkgLocation::Remote(url) => {
let path = self.cached_path(paths);
if !force && path.exists() {
self.data
.fill(PkgData::new(&tokio::fs::read_to_string(path).await?));
} else {
let url = url.as_ref().expect("URL for remote package missing");
let text = download::text(url, client).await?;
tokio::fs::write(&path, &text).await?;
self.data.fill(PkgData::new(&text));
}
}
PkgLocation::Core => {
let contents = get_core_package(&self.id)
.ok_or(anyhow!("Package is not a core package"))?;
self.data.fill(PkgData::new(contents));
}
};
}
Ok(())
}
pub async fn parse(&mut self, paths: &Paths, client: &Client) -> anyhow::Result<()> {
self.ensure_loaded(paths, false, client).await?;
let data = self.data.get_mut();
if data.contents.is_full() {
return Ok(());
}
match self.content_type {
PackageContentType::Script => {
let parsed = lex_and_parse(&data.get_text())?;
data.contents.fill(PkgContents::Script(parsed));
}
PackageContentType::Declarative => {
let contents = deserialize_declarative_package(&data.get_text())
.context("Failed to deserialize declarative package")?;
data.contents
.fill(PkgContents::Declarative(Box::new(contents)));
}
}
Ok(())
}
pub async fn get_metadata<'a>(
&'a mut self,
paths: &Paths,
client: &Client,
) -> anyhow::Result<&'a PackageMetadata> {
self.parse(paths, client).await.context("Failed to parse")?;
let data = self.data.get_mut();
match self.content_type {
PackageContentType::Script => {
let parsed = data.contents.get().get_script_contents();
if data.metadata.is_empty() {
let metadata = eval_metadata(parsed).context("Failed to evaluate metadata")?;
data.metadata.fill(metadata);
}
Ok(data.metadata.get())
}
PackageContentType::Declarative => {
let contents = data.contents.get().get_declarative_contents();
Ok(&contents.meta)
}
}
}
pub async fn get_properties<'a>(
&'a mut self,
paths: &Paths,
client: &Client,
) -> anyhow::Result<&'a PackageProperties> {
self.parse(paths, client).await.context("Failed to parse")?;
let data = self.data.get_mut();
match self.content_type {
PackageContentType::Script => {
let parsed = data.contents.get().get_script_contents();
if data.properties.is_empty() {
let properties =
eval_properties(parsed).context("Failed to evaluate properties")?;
data.properties.fill(properties);
}
Ok(data.properties.get())
}
PackageContentType::Declarative => {
let contents = data.contents.get().get_declarative_contents();
Ok(&contents.properties)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_id() {
let package = Package::new(
PackageID::from("sodium"),
PkgLocation::Remote(None),
PackageContentType::Script,
);
assert_eq!(package.filename(), "sodium".to_string() + PKG_EXTENSION);
let package = Package::new(
PackageID::from("fabriclike-api"),
PkgLocation::Remote(None),
PackageContentType::Script,
);
assert_eq!(
package.filename(),
"fabriclike-api".to_string() + PKG_EXTENSION
);
}
}