From e7cf5c36d6d928f2e5dfff9dca1c0fe41fe58569 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Mon, 2 Feb 2026 11:49:16 +0000 Subject: [PATCH] pr 52 --- crates/prometeu-compiler/src/common/config.rs | 16 +- crates/prometeu-compiler/src/compiler.rs | 8 + crates/prometeu-compiler/src/lib.rs | 1 + crates/prometeu-compiler/src/manifest.rs | 404 ++++++++++++++++++ ... specs.ms => PBS - prometeu.json specs.md} | 0 docs/specs/pbs/files/PRs para Junie.md | 40 -- test-cartridges/canonical/prometeu.json | 2 + test-cartridges/sdk/prometeu.json | 5 +- test-cartridges/test01/prometeu.json | 5 +- 9 files changed, 435 insertions(+), 46 deletions(-) create mode 100644 crates/prometeu-compiler/src/manifest.rs rename docs/specs/pbs/{PBS - prometeu.json specs.ms => PBS - prometeu.json specs.md} (100%) diff --git a/crates/prometeu-compiler/src/common/config.rs b/crates/prometeu-compiler/src/common/config.rs index 670598dc..bb9aa13a 100644 --- a/crates/prometeu-compiler/src/common/config.rs +++ b/crates/prometeu-compiler/src/common/config.rs @@ -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 { 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")); } diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index a51af6b0..a2d04910 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -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" }"#, diff --git a/crates/prometeu-compiler/src/lib.rs b/crates/prometeu-compiler/src/lib.rs index 80cf8222..c94396e3 100644 --- a/crates/prometeu-compiler/src/lib.rs +++ b/crates/prometeu-compiler/src/lib.rs @@ -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}; diff --git a/crates/prometeu-compiler/src/manifest.rs b/crates/prometeu-compiler/src/manifest.rs new file mode 100644 index 00000000..c65cb354 --- /dev/null +++ b/crates/prometeu-compiler/src/manifest.rs @@ -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, + pub git: Option, + pub version: Option, +} + +#[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, +} + +#[derive(Debug)] +pub enum ManifestError { + Io(std::io::Error), + Json { + path: PathBuf, + error: serde_json::Error, + }, + Validation { + path: PathBuf, + message: String, + pointer: Option, + }, +} + +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 { + 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), + } + } +} diff --git a/docs/specs/pbs/PBS - prometeu.json specs.ms b/docs/specs/pbs/PBS - prometeu.json specs.md similarity index 100% rename from docs/specs/pbs/PBS - prometeu.json specs.ms rename to docs/specs/pbs/PBS - prometeu.json specs.md diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index 56332f05..c535f633 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -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` -* 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` -* 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. diff --git a/test-cartridges/canonical/prometeu.json b/test-cartridges/canonical/prometeu.json index 42bdeb4d..f9d3248c 100644 --- a/test-cartridges/canonical/prometeu.json +++ b/test-cartridges/canonical/prometeu.json @@ -1,4 +1,6 @@ { + "name": "canonical", + "version": "0.1.0", "script_fe": "pbs", "entry": "src/main.pbs" } diff --git a/test-cartridges/sdk/prometeu.json b/test-cartridges/sdk/prometeu.json index 660136cb..6fb932c6 100644 --- a/test-cartridges/sdk/prometeu.json +++ b/test-cartridges/sdk/prometeu.json @@ -1,5 +1,6 @@ { - "project": "sdk", + "name": "sdk", + "version": "0.1.0", "script_fe": "pbs", - "produces": "lib" + "kind": "lib" } \ No newline at end of file diff --git a/test-cartridges/test01/prometeu.json b/test-cartridges/test01/prometeu.json index 56254530..de64b158 100644 --- a/test-cartridges/test01/prometeu.json +++ b/test-cartridges/test01/prometeu.json @@ -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": {