This commit is contained in:
Nilton Constantino 2026-02-02 11:49:16 +00:00
parent 7f831d8d37
commit e7cf5c36d6
No known key found for this signature in database
9 changed files with 435 additions and 46 deletions

View File

@ -1,9 +1,12 @@
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::manifest::Manifest;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectConfig {
#[serde(flatten)]
pub manifest: Manifest,
pub script_fe: String,
pub entry: PathBuf,
}
@ -11,8 +14,14 @@ pub struct ProjectConfig {
impl ProjectConfig {
pub fn load(project_dir: &Path) -> Result<Self> {
let config_path = project_dir.join("prometeu.json");
let content = std::fs::read_to_string(config_path)?;
let config: ProjectConfig = serde_json::from_str(&content)?;
let content = std::fs::read_to_string(&config_path)?;
let config: ProjectConfig = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("JSON error in {:?}: {}", config_path, e))?;
// Use manifest validation
crate::manifest::load_manifest(project_dir)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(config)
}
}
@ -30,6 +39,8 @@ mod tests {
fs::write(
config_path,
r#"{
"name": "test_project",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
@ -37,6 +48,7 @@ mod tests {
.unwrap();
let config = ProjectConfig::load(dir.path()).unwrap();
assert_eq!(config.manifest.name, "test_project");
assert_eq!(config.script_fe, "pbs");
assert_eq!(config.entry, PathBuf::from("main.pbs"));
}

View File

@ -117,6 +117,8 @@ mod tests {
fs::write(
config_path,
r#"{
"name": "invalid_fe",
"version": "0.1.0",
"script_fe": "invalid",
"entry": "main.pbs"
}"#,
@ -136,6 +138,8 @@ mod tests {
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "hip_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
@ -174,6 +178,8 @@ mod tests {
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "golden_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
@ -379,6 +385,8 @@ mod tests {
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "resolution_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main.pbs"
}"#,

View File

@ -44,6 +44,7 @@ pub mod lowering;
pub mod backend;
pub mod frontends;
pub mod compiler;
pub mod manifest;
use anyhow::Result;
use clap::{Parser, Subcommand};

View File

@ -0,0 +1,404 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::fs;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ManifestKind {
App,
Lib,
System,
}
impl Default for ManifestKind {
fn default() -> Self {
Self::App
}
}
pub type Alias = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum DependencySpec {
Path(String),
Full(FullDependencySpec),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FullDependencySpec {
pub path: Option<String>,
pub git: Option<String>,
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Manifest {
pub name: String,
pub version: String,
#[serde(default)]
pub kind: ManifestKind,
#[serde(default)]
pub dependencies: HashMap<Alias, DependencySpec>,
}
#[derive(Debug)]
pub enum ManifestError {
Io(std::io::Error),
Json {
path: PathBuf,
error: serde_json::Error,
},
Validation {
path: PathBuf,
message: String,
pointer: Option<String>,
},
}
impl std::fmt::Display for ManifestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ManifestError::Io(e) => write!(f, "IO error: {}", e),
ManifestError::Json { path, error } => {
write!(f, "JSON error in {}: {}", path.display(), error)
}
ManifestError::Validation { path, message, pointer } => {
write!(f, "Validation error in {}: {}", path.display(), message)?;
if let Some(p) = pointer {
write!(f, " (at {})", p)?;
}
Ok(())
}
}
}
}
impl std::error::Error for ManifestError {}
pub fn load_manifest(project_root: &Path) -> Result<Manifest, ManifestError> {
let manifest_path = project_root.join("prometeu.json");
let content = fs::read_to_string(&manifest_path).map_err(ManifestError::Io)?;
let manifest: Manifest = serde_json::from_str(&content).map_err(|e| ManifestError::Json {
path: manifest_path.clone(),
error: e,
})?;
validate_manifest(&manifest, &manifest_path)?;
Ok(manifest)
}
fn validate_manifest(manifest: &Manifest, path: &Path) -> Result<(), ManifestError> {
// Validate name
if manifest.name.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: "Project name cannot be empty".into(),
pointer: Some("/name".into()),
});
}
if manifest.name.chars().any(|c| c.is_whitespace()) {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: "Project name cannot contain whitespace".into(),
pointer: Some("/name".into()),
});
}
// Validate version (basic check, could be more thorough if we want to enforce semver now)
if manifest.version.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: "Project version cannot be empty".into(),
pointer: Some("/version".into()),
});
}
// Validate dependencies
for (alias, spec) in &manifest.dependencies {
if alias.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: "Dependency alias cannot be empty".into(),
pointer: Some("/dependencies".into()), // Best effort pointer
});
}
if alias.chars().any(|c| c.is_whitespace()) {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Dependency alias '{}' cannot contain whitespace", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
match spec {
DependencySpec::Path(p) => {
if p.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Path for dependency '{}' cannot be empty", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
}
DependencySpec::Full(full) => {
match (full.path.as_ref(), full.git.as_ref()) {
(Some(_), Some(_)) => {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Dependency '{}' must specify exactly one source (path or git), but both were found", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
(None, None) => {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Dependency '{}' must specify exactly one source (path or git), but none were found", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
(Some(p), None) => {
if p.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Path for dependency '{}' cannot be empty", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
}
(None, Some(g)) => {
if g.trim().is_empty() {
return Err(ManifestError::Validation {
path: path.to_path_buf(),
message: format!("Git URL for dependency '{}' cannot be empty", alias),
pointer: Some(format!("/dependencies/{}", alias)),
});
}
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs;
#[test]
fn test_parse_minimal_manifest() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "my_project",
"version": "0.1.0"
}"#,
)
.unwrap();
let manifest = load_manifest(dir.path()).unwrap();
assert_eq!(manifest.name, "my_project");
assert_eq!(manifest.version, "0.1.0");
assert_eq!(manifest.kind, ManifestKind::App);
assert!(manifest.dependencies.is_empty());
}
#[test]
fn test_parse_full_manifest() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "full_project",
"version": "1.2.3",
"kind": "lib",
"dependencies": {
"std": "../std",
"core": {
"git": "https://github.com/prometeu/core",
"version": "v1.0"
}
}
}"#,
)
.unwrap();
let manifest = load_manifest(dir.path()).unwrap();
assert_eq!(manifest.name, "full_project");
assert_eq!(manifest.version, "1.2.3");
assert_eq!(manifest.kind, ManifestKind::Lib);
assert_eq!(manifest.dependencies.len(), 2);
match manifest.dependencies.get("std").unwrap() {
DependencySpec::Path(p) => assert_eq!(p, "../std"),
_ => panic!("Expected path dependency"),
}
match manifest.dependencies.get("core").unwrap() {
DependencySpec::Full(full) => {
assert_eq!(full.git.as_ref().unwrap(), "https://github.com/prometeu/core");
assert_eq!(full.version.as_ref().unwrap(), "v1.0");
assert!(full.path.is_none());
}
_ => panic!("Expected full dependency"),
}
}
#[test]
fn test_missing_name_error() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"version": "0.1.0"
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Json { .. }) => {}
_ => panic!("Expected JSON error due to missing name, got {:?}", result),
}
}
#[test]
fn test_invalid_name_error() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "my project",
"version": "0.1.0"
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Validation { message, pointer, .. }) => {
assert!(message.contains("whitespace"));
assert_eq!(pointer.unwrap(), "/name");
}
_ => panic!("Expected validation error due to invalid name, got {:?}", result),
}
}
#[test]
fn test_invalid_dependency_shape_both_sources() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "test",
"version": "0.1.0",
"dependencies": {
"bad": {
"path": "./here",
"git": "https://there"
}
}
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Validation { message, pointer, .. }) => {
assert!(message.contains("exactly one source"));
assert_eq!(pointer.unwrap(), "/dependencies/bad");
}
_ => panic!("Expected validation error due to both sources, got {:?}", result),
}
}
#[test]
fn test_invalid_dependency_shape_no_source() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "test",
"version": "0.1.0",
"dependencies": {
"bad": {
"version": "1.0.0"
}
}
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Validation { message, pointer, .. }) => {
assert!(message.contains("exactly one source"));
assert_eq!(pointer.unwrap(), "/dependencies/bad");
}
_ => panic!("Expected validation error due to no source, got {:?}", result),
}
}
#[test]
fn test_invalid_dependency_empty_path() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "test",
"version": "0.1.0",
"dependencies": {
"empty": ""
}
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Validation { message, .. }) => {
assert!(message.contains("cannot be empty"));
}
_ => panic!("Expected validation error due to empty path, got {:?}", result),
}
}
#[test]
fn test_invalid_dependency_alias_whitespace() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("prometeu.json");
fs::write(
&manifest_path,
r#"{
"name": "test",
"version": "0.1.0",
"dependencies": {
"bad alias": "../std"
}
}"#,
)
.unwrap();
let result = load_manifest(dir.path());
match result {
Err(ManifestError::Validation { message, .. }) => {
assert!(message.contains("whitespace"));
}
_ => panic!("Expected validation error due to whitespace in alias, got {:?}", result),
}
}
}

View File

@ -1,43 +1,3 @@
## PR-09 — Add `prometeu.json` manifest parser + schema validation
**Why:** Dependency resolution cannot exist without a stable project manifest.
### Scope
* Implement `prometeu_compiler::manifest` module:
* `Manifest` struct mirroring the spec fields:
* `name`, `version`, `kind`
* `dependencies: HashMap<Alias, DependencySpec>`
* Support `DependencySpec` variants:
* `path`
* `git` (+ optional `version`)
* Validate:
* required fields present
* dependency entry must specify exactly one source (`path` or `git`)
* dependency aliases must be unique
* basic name rules (non-empty, no whitespace)
### Deliverables
* `load_manifest(project_root) -> Result<Manifest, ManifestError>`
* Diagnostic errors with file path + JSON pointer (or best-effort context)
### Tests
* parse minimal manifest
* missing name/version errors
* invalid dependency shape errors
### Acceptance
* Compiler can reliably load + validate `prometeu.json`.
---
## PR-10 — Dependency Resolver v0: build a resolved project graph
**Why:** We need a deterministic **module graph** from manifest(s) before compiling.

View File

@ -1,4 +1,6 @@
{
"name": "canonical",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main.pbs"
}

View File

@ -1,5 +1,6 @@
{
"project": "sdk",
"name": "sdk",
"version": "0.1.0",
"script_fe": "pbs",
"produces": "lib"
"kind": "lib"
}

View File

@ -1,7 +1,8 @@
{
"project": "test01",
"name": "test01",
"version": "0.1.0",
"script_fe": "pbs",
"produces": "app",
"kind": "app",
"entry": "src/main.pbs",
"out": "build/program.pbc",
"dependencies": {