use anyhow::Result;
use glob::glob;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PackConventions {
pub template_patterns: &'static [&'static str],
pub rdf_patterns: &'static [&'static str],
pub query_patterns: &'static [&'static str],
pub shape_patterns: &'static [&'static str],
}
impl Default for PackConventions {
fn default() -> Self {
Self {
template_patterns: &["templates/**/*.tmpl", "templates/**/*.tera"],
rdf_patterns: &[
"templates/**/graphs/*.ttl",
"templates/**/graphs/*.rdf",
"templates/**/graphs/*.jsonld"
],
query_patterns: &["templates/**/queries/*.rq", "templates/**/queries/*.sparql"],
shape_patterns: &[
"templates/**/graphs/shapes/*.shacl.ttl",
"templates/**/shapes/*.ttl"
],
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RpackManifest {
#[serde(rename = "rpack")]
pub metadata: RpackMetadata,
#[serde(default)]
pub dependencies: BTreeMap<String, String>,
#[serde(default)]
pub templates: TemplatesConfig,
#[serde(default)]
pub macros: MacrosConfig,
#[serde(default)]
pub rdf: RdfConfig,
#[serde(default)]
pub queries: QueriesConfig,
#[serde(default)]
pub shapes: ShapesConfig,
#[serde(default)]
pub preset: PresetConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RpackMetadata {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub license: String,
pub rgen_compat: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct TemplatesConfig {
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default)]
pub includes: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct MacrosConfig {
#[serde(default)]
pub paths: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RdfConfig {
#[serde(default)]
pub base: Option<String>,
#[serde(default)]
pub prefixes: BTreeMap<String, String>,
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default)]
pub inline: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct QueriesConfig {
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default)]
pub aliases: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ShapesConfig {
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PresetConfig {
#[serde(default)]
pub config: Option<PathBuf>,
#[serde(default)]
pub vars: BTreeMap<String, String>,
}
fn discover_files(base_path: &Path, patterns: &[&str]) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for pattern in patterns {
let full_pattern = base_path.join(pattern);
for entry in glob(&full_pattern.to_string_lossy())? {
files.push(entry?);
}
}
files.sort(); Ok(files)
}
impl RpackManifest {
pub fn load_from_file(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let manifest: RpackManifest = toml::from_str(&content)?;
Ok(manifest)
}
pub fn discover_templates(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
let patterns = if self.templates.patterns.is_empty() {
PackConventions::default().template_patterns
} else {
&self
.templates
.patterns
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
};
discover_files(base_path, patterns)
}
pub fn discover_rdf_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
let patterns = if self.rdf.patterns.is_empty() {
PackConventions::default().rdf_patterns
} else {
&self
.rdf
.patterns
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
};
discover_files(base_path, patterns)
}
pub fn discover_query_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
let patterns = if self.queries.patterns.is_empty() {
PackConventions::default().query_patterns
} else {
&self
.queries
.patterns
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
};
discover_files(base_path, patterns)
}
pub fn discover_shape_files(&self, base_path: &Path) -> Result<Vec<PathBuf>> {
let patterns = if self.shapes.patterns.is_empty() {
PackConventions::default().shape_patterns
} else {
&self
.shapes
.patterns
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
};
discover_files(base_path, patterns)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_manifest_parsing() {
let toml_content = r#"
[rpack]
id = "io.rgen.rust.cli-subcommand"
name = "Rust CLI subcommand"
version = "0.1.0"
description = "Generate clap subcommands"
license = "MIT"
rgen_compat = ">=0.1 <0.2"
[dependencies]
"io.rgen.macros.std" = "^0.1"
[templates]
patterns = ["cli/subcommand/*.tmpl"]
includes = ["macros/**/*.tera"]
[rdf]
base = "http://example.org/"
prefixes.ex = "http://example.org/"
patterns = ["templates/**/graphs/*.ttl"]
inline = ["@prefix ex: <http://example.org/> . ex:Foo a ex:Type ."]
[queries]
patterns = ["../queries/*.rq"]
aliases.component_by_name = "../queries/component_by_name.rq"
[shapes]
patterns = ["../shapes/*.ttl"]
[preset]
config = "../preset/rgen.toml"
vars = { author = "Acme", license = "MIT" }
"#;
let manifest: RpackManifest = toml::from_str(toml_content).unwrap();
assert_eq!(manifest.metadata.id, "io.rgen.rust.cli-subcommand");
assert_eq!(manifest.metadata.name, "Rust CLI subcommand");
assert_eq!(manifest.metadata.version, "0.1.0");
assert_eq!(manifest.templates.patterns.len(), 1);
assert_eq!(manifest.rdf.patterns.len(), 1);
assert_eq!(manifest.queries.aliases.len(), 1);
}
#[test]
fn test_manifest_load_from_file() {
let mut temp_file = NamedTempFile::new().unwrap();
let toml_content = r#"
[rpack]
id = "test"
name = "Test"
version = "0.1.0"
description = "Test"
license = "MIT"
rgen_compat = ">=0.1 <0.2"
"#;
temp_file.write_all(toml_content.as_bytes()).unwrap();
let manifest = RpackManifest::load_from_file(&temp_file.path().to_path_buf()).unwrap();
assert_eq!(manifest.metadata.id, "test");
}
}