use std::collections::HashMap;
use std::fs;
use std::path::Path;
const DEFAULT_VERSION: &str = env!("CARGO_PKG_VERSION", "0.2.0");
#[derive(Debug, Clone, Default)]
pub struct Package {
pub name: String,
pub version: String,
pub description: Option<String>,
pub main: String,
pub dependencies: HashMap<String, Dependency>,
}
#[derive(Debug, Clone)]
pub enum Dependency {
Path(String),
Version(String),
}
#[derive(Debug)]
pub enum PackageError {
IoError(std::io::Error),
ParseError(String),
InvalidField(String),
}
impl std::fmt::Display for PackageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageError::IoError(e) => write!(f, "IO error: {}", e),
PackageError::ParseError(msg) => write!(f, "Parse error: {}", msg),
PackageError::InvalidField(field) => write!(f, "Invalid field: {}", field),
}
}
}
impl std::error::Error for PackageError {}
impl From<std::io::Error> for PackageError {
fn from(e: std::io::Error) -> Self {
PackageError::IoError(e)
}
}
impl Package {
pub fn new(name: &str) -> Self {
Package {
name: name.to_string(),
version: DEFAULT_VERSION.to_string(),
description: None,
main: "src/main.sl".to_string(),
dependencies: HashMap::new(),
}
}
pub fn load(path: &Path) -> Result<Self, PackageError> {
let content = fs::read_to_string(path)?;
Self::parse(&content)
}
pub fn parse(content: &str) -> Result<Self, PackageError> {
let mut package = Package::default();
let mut current_section: Option<&str> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
let section = &line[1..line.len() - 1];
current_section = Some(match section {
"package" => "package",
"dependencies" => "dependencies",
_ => {
return Err(PackageError::ParseError(format!(
"Unknown section: {}",
section
)))
}
});
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
match current_section {
Some("package") => {
let value = parse_string_value(value)?;
match key {
"name" => package.name = value,
"version" => package.version = value,
"description" => package.description = Some(value),
"main" => package.main = value,
_ => {
return Err(PackageError::InvalidField(format!("package.{}", key)))
}
}
}
Some("dependencies") => {
let dep = parse_dependency(value)?;
package.dependencies.insert(key.to_string(), dep);
}
None => {
return Err(PackageError::ParseError(
"Key-value outside of section".to_string(),
))
}
_ => {}
}
}
}
if package.name.is_empty() {
return Err(PackageError::ParseError(
"Missing required field: package.name".to_string(),
));
}
Ok(package)
}
pub fn find(start_dir: &Path) -> Option<std::path::PathBuf> {
let mut current = start_dir.to_path_buf();
loop {
let package_file = current.join("soli.toml");
if package_file.exists() {
return Some(package_file);
}
if !current.pop() {
return None;
}
}
}
}
fn parse_string_value(value: &str) -> Result<String, PackageError> {
let value = value.trim();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
Ok(value[1..value.len() - 1].to_string())
} else {
Ok(value.to_string())
}
}
fn parse_dependency(value: &str) -> Result<Dependency, PackageError> {
let value = value.trim();
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
let path = &value[1..value.len() - 1];
if path.starts_with('.') || path.starts_with('/') || path.contains('/') {
return Ok(Dependency::Path(path.to_string()));
} else {
return Ok(Dependency::Version(path.to_string()));
}
}
if value.starts_with('{') && value.ends_with('}') {
let inner = &value[1..value.len() - 1].trim();
if let Some((key, val)) = inner.split_once('=') {
let key = key.trim();
let val = parse_string_value(val.trim())?;
match key {
"path" => return Ok(Dependency::Path(val)),
"version" => return Ok(Dependency::Version(val)),
_ => return Err(PackageError::InvalidField(format!("dependency.{}", key))),
}
}
}
Err(PackageError::ParseError(format!(
"Invalid dependency value: {}",
value
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_package() {
let content = r#"
[package]
name = "my-app"
version = "1.0.0"
main = "src/main.sl"
"#;
let pkg = Package::parse(content).unwrap();
assert_eq!(pkg.name, "my-app");
assert_eq!(pkg.version, "1.0.0");
assert_eq!(pkg.main, "src/main.sl");
}
#[test]
fn test_parse_with_dependencies() {
let content = r#"
[package]
name = "my-app"
version = "0.2.0"
[dependencies]
utils = "./lib/utils"
http = { path = "../http-lib" }
"#;
let pkg = Package::parse(content).unwrap();
assert_eq!(pkg.name, "my-app");
assert_eq!(pkg.dependencies.len(), 2);
match &pkg.dependencies["utils"] {
Dependency::Path(p) => assert_eq!(p, "./lib/utils"),
_ => panic!("Expected path dependency"),
}
match &pkg.dependencies["http"] {
Dependency::Path(p) => assert_eq!(p, "../http-lib"),
_ => panic!("Expected path dependency"),
}
}
}