use std::collections::BTreeMap;
use std::str::FromStr;
use super::version::{Version, VersionReq};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageManifest {
pub name: String,
pub version: Version,
pub description: Option<String>,
pub entry: String,
pub dependencies: BTreeMap<String, VersionReq>,
pub dev_dependencies: BTreeMap<String, VersionReq>,
pub scripts: BTreeMap<String, String>,
pub app: AppManifest,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AppManifest {
pub name: Option<String>,
pub identifier: Option<String>,
pub icon: Option<String>,
pub assets: Option<Vec<String>>,
pub console: bool,
pub publisher: Option<String>,
pub copyright: Option<String>,
}
impl AppManifest {
fn is_empty(&self) -> bool {
*self == Self::default()
}
}
fn parse_string_list(raw: &str) -> Vec<String> {
let inner = raw.trim();
let inner = inner
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
.unwrap_or(inner);
inner
.split(',')
.map(|item| item.trim().trim_matches('"').trim_matches('\'').to_string())
.filter(|item| !item.is_empty())
.collect()
}
impl Default for PackageManifest {
fn default() -> Self {
Self {
name: "unnamed_pkg".to_string(),
version: Version::new(0, 1, 0),
description: None,
entry: "main.nyk".to_string(),
dependencies: BTreeMap::new(),
dev_dependencies: BTreeMap::new(),
scripts: BTreeMap::new(),
app: AppManifest::default(),
}
}
}
impl PackageManifest {
pub fn new(name: impl Into<String>, version: Version) -> Self {
Self {
name: name.into(),
version,
..Default::default()
}
}
pub fn add_dependency(&mut self, name: impl Into<String>, req: VersionReq) {
self.dependencies.insert(name.into(), req);
}
pub fn parse(content: &str) -> Result<Self, String> {
let mut manifest = Self::default();
let mut current_section = "package";
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let sec = &trimmed[1..trimmed.len() - 1].trim();
current_section = match *sec {
"package" => "package",
"dependencies" => "dependencies",
"dev-dependencies" | "dev_dependencies" => "dev-dependencies",
"scripts" => "scripts",
"app" => "app",
_ => "unknown",
};
continue;
}
let Some((key, val_raw)) = trimmed.split_once('=') else {
continue;
};
let key = key.trim();
let val = val_raw.trim().trim_matches('"').trim_matches('\'');
match current_section {
"package" => match key {
"name" => manifest.name = val.to_string(),
"version" => manifest.version = Version::from_str(val)?,
"description" => manifest.description = Some(val.to_string()),
"entry" => manifest.entry = val.to_string(),
_ => {}
},
"dependencies" => {
let req = VersionReq::parse(val)?;
manifest.dependencies.insert(key.to_string(), req);
}
"dev-dependencies" => {
let req = VersionReq::parse(val)?;
manifest.dev_dependencies.insert(key.to_string(), req);
}
"scripts" => {
manifest.scripts.insert(key.to_string(), val.to_string());
}
"app" => {
let app = &mut manifest.app;
match key {
"name" => app.name = Some(val.to_string()),
"identifier" => app.identifier = Some(val.to_string()),
"icon" => app.icon = Some(val.to_string()),
"assets" => app.assets = Some(parse_string_list(val_raw)),
"console" => app.console = val == "true",
"publisher" => app.publisher = Some(val.to_string()),
"copyright" => app.copyright = Some(val.to_string()),
_ => {}
}
}
_ => {}
}
}
Ok(manifest)
}
pub fn serialize(&self) -> String {
let mut out = String::new();
out.push_str("[package]\n");
out.push_str(&format!("name = \"{}\"\n", self.name));
out.push_str(&format!("version = \"{}\"\n", self.version));
if let Some(desc) = &self.description {
out.push_str(&format!("description = \"{}\"\n", desc));
}
out.push_str(&format!("entry = \"{}\"\n\n", self.entry));
if !self.dependencies.is_empty() {
out.push_str("[dependencies]\n");
for (k, v) in &self.dependencies {
out.push_str(&format!("{} = \"{}\"\n", k, v));
}
out.push('\n');
}
if !self.dev_dependencies.is_empty() {
out.push_str("[dev-dependencies]\n");
for (k, v) in &self.dev_dependencies {
out.push_str(&format!("{} = \"{}\"\n", k, v));
}
out.push('\n');
}
if !self.scripts.is_empty() {
out.push_str("[scripts]\n");
for (k, v) in &self.scripts {
out.push_str(&format!("{} = \"{}\"\n", k, v));
}
out.push('\n');
}
if !self.app.is_empty() {
let app = &self.app;
out.push_str("[app]\n");
let fields = [
("name", &app.name),
("identifier", &app.identifier),
("icon", &app.icon),
("publisher", &app.publisher),
("copyright", &app.copyright),
];
for (key, value) in fields {
if let Some(value) = value {
out.push_str(&format!("{} = \"{}\"\n", key, value));
}
}
if let Some(assets) = &app.assets {
let quoted: Vec<String> = assets.iter().map(|a| format!("\"{}\"", a)).collect();
out.push_str(&format!("assets = [{}]\n", quoted.join(", ")));
}
if app.console {
out.push_str("console = true\n");
}
out.push('\n');
}
out
}
}