use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct GgenConfig {
pub project: Project,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
pub graph: Option<Graph>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<String, Dependency>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dev_dependencies: BTreeMap<String, Dependency>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub build_dependencies: BTreeMap<String, Dependency>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub target: BTreeMap<String, TargetDependencies>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ontology: Option<Ontology>,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Templates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub generators: Option<Generators>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lifecycle: Option<Lifecycle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugins: Option<Plugins>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profiles: Option<Profiles>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<Validation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub prefixes: BTreeMap<String, String>,
#[serde(rename = "rdf", skip_serializing_if = "Option::is_none")]
pub rdf: Option<RdfConfig>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub vars: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Project {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub project_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Workspace {
#[serde(default)]
pub members: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, Dependency>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub graph: Option<WorkspaceGraph>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceGraph {
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Graph {
#[serde(skip_serializing_if = "Option::is_none")]
pub strategy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conflict_resolution: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub queries: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub features: Option<BTreeMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Simple(String),
Detailed(DetailedDependency),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DetailedDependency {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub features: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub optional: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_features: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetDependencies {
#[serde(default)]
pub dependencies: BTreeMap<String, Dependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Ontology {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shapes: Vec<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constitution: Option<Constitution>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Constitution {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub custom: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enforce_strict: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fail_on_warning: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Templates {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub paths: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vars: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compose: Option<BTreeMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guards: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub queries: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Generators {
#[serde(skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub installed: BTreeMap<String, InstalledGenerator>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pipeline: Vec<GeneratorPipeline>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<GeneratorHooks>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledGenerator {
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorPipeline {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>,
#[serde(default)]
pub steps: Vec<PipelineStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineStep {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parser: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratorHooks {
#[serde(skip_serializing_if = "Option::is_none")]
pub before_generate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_generate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Lifecycle {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub phases: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<BTreeMap<String, Vec<String>>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tasks: BTreeMap<String, LifecycleTask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel: Option<BTreeMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LifecycleTask {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Plugins {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub paths: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub installed: BTreeMap<String, InstalledPlugin>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub config: BTreeMap<String, BTreeMap<String, toml::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<BTreeMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<BTreeMap<String, PluginPermissions>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPlugin {
pub version: String,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginPermissions {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub filesystem: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub network: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exec: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Profiles {
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dev: Option<Profile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub production: Option<Profile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test: Option<Profile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ci: Option<Profile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bench: Option<Profile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Profile {
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub optimization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debug_assertions: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overflow_checks: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lto: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strip: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub codegen_units: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_coverage: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_threads: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, Dependency>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<ProfileTemplates>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ontology: Option<ProfileOntology>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lifecycle: Option<ProfileLifecycle>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileTemplates {
#[serde(skip_serializing_if = "Option::is_none")]
pub vars: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileOntology {
#[serde(skip_serializing_if = "Option::is_none")]
pub constitution: Option<Constitution>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileLifecycle {
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<BTreeMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub changelog: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build: Option<BuildMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<PackageMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub rustc_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub llvm_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_triple: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageMetadata {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub readme: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Validation {
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rust_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<DependencyValidation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<QualityThresholds>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_duplicates: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_yanked: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_checksums: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityThresholds {
#[serde(skip_serializing_if = "Option::is_none")]
pub min_coverage: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_complexity: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_function_lines: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RdfConfig {
#[serde(default)]
pub files: Vec<PathBuf>,
#[serde(default)]
pub inline: Vec<String>,
}
impl GgenConfig {
pub fn validate(&self) -> Result<(), String> {
if self.project.name.is_empty() {
return Err("Project name cannot be empty".to_string());
}
if !self.is_valid_version(&self.project.version) {
return Err(format!("Invalid version format: {}", self.project.version));
}
if let Some(ref workspace) = self.workspace {
if workspace.members.is_empty() {
return Err("Workspace members cannot be empty".to_string());
}
}
if let Some(ref validation) = self.validation {
if let Some(ref min_version) = validation.min_rust_version {
if !self.is_valid_version(min_version) {
return Err(format!("Invalid min_rust_version: {}", min_version));
}
}
}
Ok(())
}
fn is_valid_version(&self, version: &str) -> bool {
let parts: Vec<&str> = version.split('.').collect();
parts.len() >= 2 && parts.iter().all(|p| p.parse::<u32>().is_ok())
}
pub fn get_profile_dependencies(&self, profile_name: &str) -> BTreeMap<String, Dependency> {
let mut deps = self.dependencies.clone();
if let Some(ref profiles) = self.profiles {
let profile = match profile_name {
"dev" => profiles.dev.as_ref(),
"production" => profiles.production.as_ref(),
"test" => profiles.test.as_ref(),
"ci" => profiles.ci.as_ref(),
"bench" => profiles.bench.as_ref(),
_ => None,
};
if let Some(profile) = profile {
if let Some(ref profile_deps) = profile.dependencies {
deps.extend(profile_deps.clone());
}
}
}
deps
}
pub fn get_profile_template_vars(&self, profile_name: &str) -> BTreeMap<String, String> {
let mut vars = self.vars.clone();
if let Some(ref templates) = self.templates {
if let Some(ref template_vars) = templates.vars {
vars.extend(template_vars.clone());
}
}
if let Some(ref profiles) = self.profiles {
let profile = match profile_name {
"dev" => profiles.dev.as_ref(),
"production" => profiles.production.as_ref(),
"test" => profiles.test.as_ref(),
"ci" => profiles.ci.as_ref(),
"bench" => profiles.bench.as_ref(),
_ => None,
};
if let Some(profile) = profile {
if let Some(ref templates) = profile.templates {
if let Some(ref profile_vars) = templates.vars {
vars.extend(profile_vars.clone());
}
}
}
}
vars
}
}
impl Default for GgenConfig {
fn default() -> Self {
Self {
project: Project {
name: "untitled".to_string(),
version: "0.1.0".to_string(),
description: None,
authors: Vec::new(),
license: None,
edition: Some("2021".to_string()),
project_type: Some("auto".to_string()),
language: Some("auto".to_string()),
uri: None,
namespace: None,
extends: None,
output_dir: None,
},
workspace: None,
graph: None,
dependencies: BTreeMap::new(),
dev_dependencies: BTreeMap::new(),
build_dependencies: BTreeMap::new(),
target: BTreeMap::new(),
ontology: None,
templates: None,
generators: None,
lifecycle: None,
plugins: None,
profiles: None,
metadata: None,
validation: None,
prefixes: BTreeMap::new(),
rdf: None,
vars: BTreeMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_is_valid() {
let config = GgenConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_version_validation() {
let config = GgenConfig::default();
assert!(config.is_valid_version("1.0.0"));
assert!(config.is_valid_version("0.1.0"));
assert!(config.is_valid_version("2.3.4"));
assert!(!config.is_valid_version("invalid"));
assert!(!config.is_valid_version("1"));
}
#[test]
fn test_empty_project_name_fails_validation() {
let mut config = GgenConfig::default();
config.project.name = String::new();
assert!(config.validate().is_err());
}
#[test]
fn test_invalid_version_fails_validation() {
let mut config = GgenConfig::default();
config.project.version = "invalid".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_profile_dependencies_merge() {
let mut config = GgenConfig::default();
config
.dependencies
.insert("base".to_string(), Dependency::Simple("1.0".to_string()));
let mut dev_deps = BTreeMap::new();
dev_deps.insert("dev-dep".to_string(), Dependency::Simple("2.0".to_string()));
config.profiles = Some(Profiles {
default: Some("dev".to_string()),
dev: Some(Profile {
extends: None,
optimization: None,
debug_assertions: None,
overflow_checks: None,
lto: None,
strip: None,
codegen_units: None,
code_coverage: None,
test_threads: None,
dependencies: Some(dev_deps),
templates: None,
ontology: None,
lifecycle: None,
}),
production: None,
test: None,
ci: None,
bench: None,
});
let deps = config.get_profile_dependencies("dev");
assert_eq!(deps.len(), 2);
assert!(deps.contains_key("base"));
assert!(deps.contains_key("dev-dep"));
}
}