pr 52
This commit is contained in:
parent
7f831d8d37
commit
e7cf5c36d6
@ -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"));
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}"#,
|
||||
|
||||
@ -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};
|
||||
|
||||
404
crates/prometeu-compiler/src/manifest.rs
Normal file
404
crates/prometeu-compiler/src/manifest.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
{
|
||||
"name": "canonical",
|
||||
"version": "0.1.0",
|
||||
"script_fe": "pbs",
|
||||
"entry": "src/main.pbs"
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"project": "sdk",
|
||||
"name": "sdk",
|
||||
"version": "0.1.0",
|
||||
"script_fe": "pbs",
|
||||
"produces": "lib"
|
||||
"kind": "lib"
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user