ggen_utils/
project_config.rs

1//! Project configuration types
2//!
3//! This module provides comprehensive configuration structures for ggen projects,
4//! implementing the full ggen.toml schema with support for workspaces, dependencies,
5//! ontology integration, templates, generators, lifecycle management, plugins, and profiles.
6//!
7//! ## Configuration Structure
8//!
9//! The `GgenConfig` structure represents the root configuration for a ggen project:
10//!
11//! - **Project**: Project metadata and settings
12//! - **Workspace**: Mono-repo configuration
13//! - **Graph**: Graph-based dependency resolution
14//! - **Dependencies**: Full Cargo.toml-like dependencies
15//! - **Ontology**: RDF/OWL integration
16//! - **Templates**: Template composition and inheritance
17//! - **Generators**: Code generation pipelines
18//! - **Lifecycle**: Build lifecycle and hooks
19//! - **Plugins**: Plugin system configuration
20//! - **Profiles**: Environment-specific overrides
21//! - **Metadata**: Package and build metadata
22//! - **Validation**: Constraints and quality thresholds
23//!
24//! ## Type-First Design
25//!
26//! This implementation uses type-first thinking to encode invariants:
27//! - Optional fields use `Option<T>` with proper defaults
28//! - `BTreeMap` for deterministic ordering (not `HashMap`)
29//! - `Result<T, Error>` for all fallible operations
30//! - Zero unsafe blocks (memory safety guaranteed)
31//!
32//! ## Examples
33//!
34//! ### Loading Configuration
35//!
36//! ```rust,no_run
37//! use ggen_utils::project_config::GgenConfig;
38//! use std::fs;
39//!
40//! # fn main() -> anyhow::Result<()> {
41//! let content = fs::read_to_string("ggen.toml")?;
42//! let config: GgenConfig = toml::from_str(&content)?;
43//!
44//! println!("Project: {} v{}", config.project.name, config.project.version);
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! ### Workspace Configuration
50//!
51//! ```rust,no_run
52//! use ggen_utils::project_config::{GgenConfig, Workspace};
53//! use std::collections::BTreeMap;
54//!
55//! let workspace = Workspace {
56//!     members: vec!["crates/*".to_string(), "packages/*".to_string()],
57//!     exclude: Some(vec!["target".to_string()]),
58//!     dependencies: Some(BTreeMap::new()),
59//!     graph: None,
60//! };
61//! ```
62
63use serde::{Deserialize, Serialize};
64use std::collections::BTreeMap;
65use std::path::PathBuf;
66
67// ============================================================================
68// Diataxis Documentation Section
69// ============================================================================
70
71/// Diataxis documentation configuration (tutorials, how-to, reference, explanations)
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "kebab-case")]
74pub struct Diataxis {
75    /// Root directory containing source documentation
76    #[serde(default = "default_diataxis_root")]
77    pub root: String,
78
79    /// Path to diataxis index/landing page
80    #[serde(default = "default_diataxis_index")]
81    pub index: String,
82
83    /// Quadrant-specific configuration
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub quadrants: Option<DiataxisQuadrants>,
86}
87
88/// Quadrant definitions
89#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90#[serde(rename_all = "kebab-case")]
91pub struct DiataxisQuadrants {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub tutorials: Option<DiataxisSection>,
94
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub reference: Option<DiataxisSection>,
97
98    #[serde(rename = "how-to", skip_serializing_if = "Option::is_none")]
99    pub how_to: Option<DiataxisSection>,
100
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub explanations: Option<DiataxisSection>,
103}
104
105/// Per-quadrant configuration
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "kebab-case")]
108pub struct DiataxisSection {
109    /// Source directory where authored docs live
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub source: Option<String>,
112
113    /// Output directory where generated docs should be written
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub output: Option<String>,
116
117    /// Navigation entries for this quadrant
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    pub navigation: Vec<DiataxisNav>,
120}
121
122/// Navigation entry within a quadrant
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124#[serde(rename_all = "kebab-case")]
125pub struct DiataxisNav {
126    /// Display title
127    pub title: String,
128
129    /// Relative path to the document
130    pub path: String,
131
132    /// Optional description or summary
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub description: Option<String>,
135}
136
137// ============================================================================
138// Root Configuration
139// ============================================================================
140
141/// Root configuration structure for ggen projects
142///
143/// Supports full ggen.toml schema with all features including workspaces,
144/// dependencies, ontology, templates, generators, lifecycle, plugins, and profiles.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "kebab-case")]
147pub struct GgenConfig {
148    /// Project metadata and settings
149    pub project: Project,
150
151    /// Workspace configuration for mono-repos
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub workspace: Option<Workspace>,
154
155    /// Graph-based dependency resolution
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub graph: Option<Graph>,
158
159    /// Project dependencies (Cargo.toml-like)
160    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
161    pub dependencies: BTreeMap<String, Dependency>,
162
163    /// Development dependencies
164    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
165    pub dev_dependencies: BTreeMap<String, Dependency>,
166
167    /// Build dependencies
168    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
169    pub build_dependencies: BTreeMap<String, Dependency>,
170
171    /// Target-specific dependencies
172    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
173    pub target: BTreeMap<String, TargetDependencies>,
174
175    /// RDF/OWL ontology configuration
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub ontology: Option<Ontology>,
178
179    /// Template configuration
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub templates: Option<Templates>,
182
183    /// Diataxis documentation configuration
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub diataxis: Option<Diataxis>,
186
187    /// Code generator configuration
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub generators: Option<Generators>,
190
191    /// Build lifecycle configuration
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub lifecycle: Option<Lifecycle>,
194
195    /// Plugin system configuration
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub plugins: Option<Plugins>,
198
199    /// Environment profiles (dev, production, test, ci, bench)
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub profiles: Option<Profiles>,
202
203    /// Package and build metadata
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub metadata: Option<Metadata>,
206
207    /// Validation rules and constraints
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub validation: Option<Validation>,
210
211    /// RDF namespace prefixes (legacy support)
212    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
213    pub prefixes: BTreeMap<String, String>,
214
215    /// Legacy RDF configuration (for backward compatibility)
216    #[serde(rename = "rdf", skip_serializing_if = "Option::is_none")]
217    pub rdf: Option<RdfConfig>,
218
219    /// Template variables (legacy support)
220    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
221    pub vars: BTreeMap<String, String>,
222}
223
224// ============================================================================
225// Project Section
226// ============================================================================
227
228/// Project metadata and settings
229#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(rename_all = "kebab-case")]
231pub struct Project {
232    /// Project name (required)
233    pub name: String,
234
235    /// Project version (required)
236    pub version: String,
237
238    /// Project description
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub description: Option<String>,
241
242    /// Project authors
243    #[serde(default, skip_serializing_if = "Vec::is_empty")]
244    pub authors: Vec<String>,
245
246    /// License identifier
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub license: Option<String>,
249
250    /// Rust edition (2015, 2018, 2021, 2024)
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub edition: Option<String>,
253
254    /// Project type (auto, library, binary, workspace)
255    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
256    pub project_type: Option<String>,
257
258    /// Primary language (auto, rust, typescript, python, multi)
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub language: Option<String>,
261
262    /// RDF URI for this project
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub uri: Option<String>,
265
266    /// Short namespace prefix for RDF queries
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub namespace: Option<String>,
269
270    /// Template to inherit from
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub extends: Option<String>,
273
274    /// Output directory (legacy support)
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub output_dir: Option<PathBuf>,
277}
278
279// ============================================================================
280// Workspace Section
281// ============================================================================
282
283/// Workspace configuration for mono-repos
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "kebab-case")]
286pub struct Workspace {
287    /// Workspace members (glob patterns supported)
288    #[serde(default)]
289    pub members: Vec<String>,
290
291    /// Paths to exclude from workspace
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub exclude: Option<Vec<String>>,
294
295    /// Shared dependencies across workspace
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub dependencies: Option<BTreeMap<String, Dependency>>,
298
299    /// Workspace-level graph queries
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub graph: Option<WorkspaceGraph>,
302}
303
304/// Workspace-level graph configuration
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct WorkspaceGraph {
307    /// SPARQL query for workspace analysis
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub query: Option<String>,
310}
311
312// ============================================================================
313// Graph Section
314// ============================================================================
315
316/// Graph-based dependency resolution configuration
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "kebab-case")]
319pub struct Graph {
320    /// Resolution strategy (smart, conservative, aggressive)
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub strategy: Option<String>,
323
324    /// Conflict resolution strategy (newest, oldest, semver-compatible)
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub conflict_resolution: Option<String>,
327
328    /// Graph-based dependency queries
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub queries: Option<BTreeMap<String, String>>,
331
332    /// Feature flags as graph nodes
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub features: Option<BTreeMap<String, Vec<String>>>,
335}
336
337// ============================================================================
338// Dependencies Section
339// ============================================================================
340
341/// Dependency specification (Cargo.toml-like)
342#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(untagged)]
344pub enum Dependency {
345    /// Simple version string
346    Simple(String),
347    /// Detailed dependency specification
348    Detailed(DetailedDependency),
349}
350
351/// Detailed dependency specification
352#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "kebab-case")]
354pub struct DetailedDependency {
355    /// Version requirement
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub version: Option<String>,
358
359    /// Feature flags to enable
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub features: Vec<String>,
362
363    /// Whether dependency is optional
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub optional: Option<bool>,
366
367    /// Default features enabled
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub default_features: Option<bool>,
370
371    /// Git repository URL
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub git: Option<String>,
374
375    /// Git branch
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub branch: Option<String>,
378
379    /// Git tag
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub tag: Option<String>,
382
383    /// Git revision
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub rev: Option<String>,
386
387    /// Local path dependency
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub path: Option<PathBuf>,
390
391    /// Alternative registry
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub registry: Option<String>,
394
395    /// Package name (if different from dependency key)
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub package: Option<String>,
398}
399
400/// Target-specific dependencies
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct TargetDependencies {
403    /// Dependencies for this target
404    #[serde(default)]
405    pub dependencies: BTreeMap<String, Dependency>,
406}
407
408// ============================================================================
409// Ontology Section
410// ============================================================================
411
412/// RDF/OWL ontology integration configuration
413#[derive(Debug, Clone, Serialize, Deserialize)]
414#[serde(rename_all = "kebab-case")]
415pub struct Ontology {
416    /// Ontology files (Turtle format)
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub files: Vec<PathBuf>,
419
420    /// Inline RDF (Turtle format)
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub inline: Option<String>,
423
424    /// SHACL shape files for validation
425    #[serde(default, skip_serializing_if = "Vec::is_empty")]
426    pub shapes: Vec<PathBuf>,
427
428    /// Constitution (invariant checks)
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub constitution: Option<Constitution>,
431}
432
433/// Constitution configuration (invariant checks)
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct Constitution {
436    /// Built-in checks (NoRetrocausation, TypeSoundness, GuardSoundness)
437    #[serde(default, skip_serializing_if = "Vec::is_empty")]
438    pub checks: Vec<String>,
439
440    /// Custom invariants (SPARQL ASK queries)
441    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
442    pub custom: BTreeMap<String, String>,
443
444    /// Enforce strict mode (production)
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub enforce_strict: Option<bool>,
447
448    /// Fail on warnings
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub fail_on_warning: Option<bool>,
451}
452
453// ============================================================================
454// Templates Section
455// ============================================================================
456
457/// Template composition and inheritance configuration
458#[derive(Debug, Clone, Serialize, Deserialize)]
459#[serde(rename_all = "kebab-case")]
460pub struct Templates {
461    /// Template search paths
462    #[serde(default, skip_serializing_if = "Vec::is_empty")]
463    pub paths: Vec<String>,
464
465    /// Default template variables
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub vars: Option<BTreeMap<String, String>>,
468
469    /// Template inheritance chain
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub extends: Option<BTreeMap<String, String>>,
472
473    /// Template composition (merge multiple templates)
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub compose: Option<BTreeMap<String, Vec<String>>>,
476
477    /// Template guards (conditional rendering)
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub guards: Option<BTreeMap<String, String>>,
480
481    /// SPARQL-driven template queries
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub queries: Option<BTreeMap<String, String>>,
484}
485
486/// Quadrant identifier for resolved diataxis sections
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
488#[serde(rename_all = "kebab-case")]
489pub enum DiataxisQuadrant {
490    Tutorials,
491    HowTo,
492    Reference,
493    Explanations,
494}
495
496impl DiataxisQuadrant {
497    /// Directory-friendly name for the quadrant
498    pub fn as_dir(&self) -> &'static str {
499        match self {
500            Self::Tutorials => "tutorials",
501            Self::HowTo => "how-to",
502            Self::Reference => "reference",
503            Self::Explanations => "explanations",
504        }
505    }
506}
507
508/// Resolved diataxis section with effective paths and navigation
509#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
510pub struct ResolvedDiataxisSection {
511    pub quadrant: DiataxisQuadrant,
512    pub source: String,
513    pub output: String,
514    pub navigation: Vec<DiataxisNav>,
515}
516
517fn default_diataxis_root() -> String {
518    "docs".to_string()
519}
520
521fn default_diataxis_index() -> String {
522    "docs/diataxis-index.md".to_string()
523}
524
525// ============================================================================
526// Generators Section
527// ============================================================================
528
529/// Code generation pipeline configuration
530#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(rename_all = "kebab-case")]
532pub struct Generators {
533    /// Generator registry URL
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub registry: Option<String>,
536
537    /// Installed generators
538    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
539    pub installed: BTreeMap<String, InstalledGenerator>,
540
541    /// Generator pipelines
542    #[serde(default, skip_serializing_if = "Vec::is_empty")]
543    pub pipeline: Vec<GeneratorPipeline>,
544
545    /// Generator hooks
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub hooks: Option<GeneratorHooks>,
548}
549
550/// Installed generator specification
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct InstalledGenerator {
553    /// Generator version
554    pub version: String,
555
556    /// Registry name
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub registry: Option<String>,
559
560    /// Git source
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub source: Option<String>,
563}
564
565/// Generator pipeline (multi-step workflow)
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct GeneratorPipeline {
568    /// Pipeline name
569    pub name: String,
570
571    /// Pipeline description
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub description: Option<String>,
574
575    /// Input files
576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
577    pub inputs: Vec<String>,
578
579    /// Pipeline steps
580    #[serde(default)]
581    pub steps: Vec<PipelineStep>,
582}
583
584/// Pipeline step specification
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct PipelineStep {
587    /// Action type (template, parse, transform, exec)
588    pub action: String,
589
590    /// Template name (for template action)
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub template: Option<String>,
593
594    /// Parser name (for parse action)
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub parser: Option<String>,
597
598    /// SPARQL query file (for transform action)
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub query: Option<String>,
601
602    /// Shell command (for exec action)
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub command: Option<String>,
605}
606
607/// Generator lifecycle hooks
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct GeneratorHooks {
610    /// Before generation hook
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub before_generate: Option<String>,
613
614    /// After generation hook
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub after_generate: Option<String>,
617
618    /// Error handler hook
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub on_error: Option<String>,
621}
622
623// ============================================================================
624// Lifecycle Section
625// ============================================================================
626
627/// Build lifecycle configuration
628#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "kebab-case")]
630pub struct Lifecycle {
631    /// Lifecycle phases
632    #[serde(default, skip_serializing_if = "Vec::is_empty")]
633    pub phases: Vec<String>,
634
635    /// Phase hooks
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub hooks: Option<BTreeMap<String, Vec<String>>>,
638
639    /// Task definitions
640    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
641    pub tasks: BTreeMap<String, LifecycleTask>,
642
643    /// Parallel execution groups
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub parallel: Option<BTreeMap<String, Vec<String>>>,
646}
647
648/// Lifecycle task definition
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct LifecycleTask {
651    /// Task description
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub description: Option<String>,
654
655    /// Task dependencies
656    #[serde(default, skip_serializing_if = "Vec::is_empty")]
657    pub dependencies: Vec<String>,
658
659    /// Shell command
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub command: Option<String>,
662
663    /// Script to run
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub script: Option<String>,
666}
667
668// ============================================================================
669// Plugins Section
670// ============================================================================
671
672/// Plugin system configuration
673#[derive(Debug, Clone, Serialize, Deserialize)]
674#[serde(rename_all = "kebab-case")]
675pub struct Plugins {
676    /// Plugin discovery paths
677    #[serde(default, skip_serializing_if = "Vec::is_empty")]
678    pub paths: Vec<String>,
679
680    /// Installed plugins
681    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
682    pub installed: BTreeMap<String, InstalledPlugin>,
683
684    /// Plugin-specific configuration
685    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
686    pub config: BTreeMap<String, BTreeMap<String, toml::Value>>,
687
688    /// Plugin lifecycle hooks
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub hooks: Option<BTreeMap<String, Vec<String>>>,
691
692    /// Plugin permissions (security sandboxing)
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub permissions: Option<BTreeMap<String, PluginPermissions>>,
695}
696
697/// Installed plugin specification
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct InstalledPlugin {
700    /// Plugin version
701    pub version: String,
702
703    /// Plugin source (path, git, registry)
704    pub source: String,
705}
706
707/// Plugin security permissions
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct PluginPermissions {
710    /// Filesystem access permissions
711    #[serde(default, skip_serializing_if = "Vec::is_empty")]
712    pub filesystem: Vec<String>,
713
714    /// Network access permissions
715    #[serde(default, skip_serializing_if = "Vec::is_empty")]
716    pub network: Vec<String>,
717
718    /// Executable permissions
719    #[serde(default, skip_serializing_if = "Vec::is_empty")]
720    pub exec: Vec<String>,
721}
722
723// ============================================================================
724// Profiles Section
725// ============================================================================
726
727/// Environment-specific profile configuration
728#[derive(Debug, Clone, Serialize, Deserialize)]
729#[serde(rename_all = "kebab-case")]
730pub struct Profiles {
731    /// Default profile
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub default: Option<String>,
734
735    /// Development profile
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub dev: Option<Profile>,
738
739    /// Production profile
740    #[serde(skip_serializing_if = "Option::is_none")]
741    pub production: Option<Profile>,
742
743    /// Testing profile
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub test: Option<Profile>,
746
747    /// CI profile
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub ci: Option<Profile>,
750
751    /// Benchmark profile
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub bench: Option<Profile>,
754}
755
756/// Profile configuration
757#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "kebab-case")]
759pub struct Profile {
760    /// Profile to extend
761    #[serde(skip_serializing_if = "Option::is_none")]
762    pub extends: Option<String>,
763
764    /// Optimization level
765    #[serde(skip_serializing_if = "Option::is_none")]
766    pub optimization: Option<String>,
767
768    /// Debug assertions
769    #[serde(skip_serializing_if = "Option::is_none")]
770    pub debug_assertions: Option<bool>,
771
772    /// Overflow checks
773    #[serde(skip_serializing_if = "Option::is_none")]
774    pub overflow_checks: Option<bool>,
775
776    /// Link-time optimization
777    #[serde(skip_serializing_if = "Option::is_none")]
778    pub lto: Option<String>,
779
780    /// Strip symbols
781    #[serde(skip_serializing_if = "Option::is_none")]
782    pub strip: Option<bool>,
783
784    /// Codegen units
785    #[serde(skip_serializing_if = "Option::is_none")]
786    pub codegen_units: Option<u32>,
787
788    /// Code coverage
789    #[serde(skip_serializing_if = "Option::is_none")]
790    pub code_coverage: Option<bool>,
791
792    /// Test threads
793    #[serde(skip_serializing_if = "Option::is_none")]
794    pub test_threads: Option<u32>,
795
796    /// Profile-specific dependencies
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub dependencies: Option<BTreeMap<String, Dependency>>,
799
800    /// Profile-specific template variables
801    #[serde(skip_serializing_if = "Option::is_none")]
802    pub templates: Option<ProfileTemplates>,
803
804    /// Profile-specific ontology configuration
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub ontology: Option<ProfileOntology>,
807
808    /// Profile-specific lifecycle hooks
809    #[serde(skip_serializing_if = "Option::is_none")]
810    pub lifecycle: Option<ProfileLifecycle>,
811}
812
813/// Profile template configuration
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct ProfileTemplates {
816    /// Template variables
817    #[serde(skip_serializing_if = "Option::is_none")]
818    pub vars: Option<BTreeMap<String, String>>,
819}
820
821/// Profile ontology configuration
822#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct ProfileOntology {
824    /// Constitution configuration
825    #[serde(skip_serializing_if = "Option::is_none")]
826    pub constitution: Option<Constitution>,
827}
828
829/// Profile lifecycle configuration
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct ProfileLifecycle {
832    /// Lifecycle hooks
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub hooks: Option<BTreeMap<String, Vec<String>>>,
835}
836
837// ============================================================================
838// Metadata Section
839// ============================================================================
840
841/// Package and build metadata
842#[derive(Debug, Clone, Serialize, Deserialize)]
843#[serde(rename_all = "kebab-case")]
844pub struct Metadata {
845    /// Project homepage
846    #[serde(skip_serializing_if = "Option::is_none")]
847    pub homepage: Option<String>,
848
849    /// Documentation URL
850    #[serde(skip_serializing_if = "Option::is_none")]
851    pub documentation: Option<String>,
852
853    /// Repository URL
854    #[serde(skip_serializing_if = "Option::is_none")]
855    pub repository: Option<String>,
856
857    /// Changelog file
858    #[serde(skip_serializing_if = "Option::is_none")]
859    pub changelog: Option<String>,
860
861    /// Build metadata
862    #[serde(skip_serializing_if = "Option::is_none")]
863    pub build: Option<BuildMetadata>,
864
865    /// Package metadata
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub package: Option<PackageMetadata>,
868}
869
870/// Build metadata
871#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct BuildMetadata {
873    /// Rust compiler version
874    #[serde(skip_serializing_if = "Option::is_none")]
875    pub rustc_version: Option<String>,
876
877    /// LLVM version
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub llvm_version: Option<String>,
880
881    /// Target triple
882    #[serde(skip_serializing_if = "Option::is_none")]
883    pub target_triple: Option<String>,
884}
885
886/// Package metadata
887#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct PackageMetadata {
889    /// Package categories
890    #[serde(default, skip_serializing_if = "Vec::is_empty")]
891    pub categories: Vec<String>,
892
893    /// Package keywords
894    #[serde(default, skip_serializing_if = "Vec::is_empty")]
895    pub keywords: Vec<String>,
896
897    /// README file
898    #[serde(skip_serializing_if = "Option::is_none")]
899    pub readme: Option<String>,
900
901    /// Files to include
902    #[serde(default, skip_serializing_if = "Vec::is_empty")]
903    pub include: Vec<String>,
904
905    /// Files to exclude
906    #[serde(default, skip_serializing_if = "Vec::is_empty")]
907    pub exclude: Vec<String>,
908}
909
910// ============================================================================
911// Validation Section
912// ============================================================================
913
914/// Validation rules and constraints
915#[derive(Debug, Clone, Serialize, Deserialize)]
916#[serde(rename_all = "kebab-case")]
917pub struct Validation {
918    /// Minimum Rust version
919    #[serde(skip_serializing_if = "Option::is_none")]
920    pub min_rust_version: Option<String>,
921
922    /// Dependency constraints
923    #[serde(skip_serializing_if = "Option::is_none")]
924    pub dependencies: Option<DependencyValidation>,
925
926    /// Code quality thresholds
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub quality: Option<QualityThresholds>,
929}
930
931/// Dependency validation rules
932#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct DependencyValidation {
934    /// Maximum duplicate dependencies
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub max_duplicates: Option<u32>,
937
938    /// Allow yanked crates
939    #[serde(skip_serializing_if = "Option::is_none")]
940    pub allow_yanked: Option<bool>,
941
942    /// Require checksums
943    #[serde(skip_serializing_if = "Option::is_none")]
944    pub require_checksums: Option<bool>,
945}
946
947/// Code quality thresholds
948#[derive(Debug, Clone, Serialize, Deserialize)]
949pub struct QualityThresholds {
950    /// Minimum code coverage percentage
951    #[serde(skip_serializing_if = "Option::is_none")]
952    pub min_coverage: Option<u32>,
953
954    /// Maximum cyclomatic complexity
955    #[serde(skip_serializing_if = "Option::is_none")]
956    pub max_complexity: Option<u32>,
957
958    /// Maximum function lines
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub max_function_lines: Option<u32>,
961}
962
963// ============================================================================
964// Legacy RDF Configuration (Backward Compatibility)
965// ============================================================================
966
967/// Legacy RDF configuration structure
968#[derive(Debug, Clone, Serialize, Deserialize)]
969pub struct RdfConfig {
970    /// RDF files
971    #[serde(default)]
972    pub files: Vec<PathBuf>,
973
974    /// Inline RDF data
975    #[serde(default)]
976    pub inline: Vec<String>,
977}
978
979// ============================================================================
980// Implementation - Validation and Builder Methods
981// ============================================================================
982
983impl GgenConfig {
984    /// Validate the configuration for consistency
985    ///
986    /// Checks:
987    /// - Required fields are present
988    /// - Version strings are valid
989    /// - Paths exist (if validation enabled)
990    /// - Dependencies are consistent
991    ///
992    /// # Errors
993    ///
994    /// Returns error if validation fails
995    pub fn validate(&self) -> Result<(), String> {
996        // Validate project name is not empty
997        if self.project.name.is_empty() {
998            return Err("Project name cannot be empty".to_string());
999        }
1000
1001        // Validate version format (basic semver check)
1002        if !self.is_valid_version(&self.project.version) {
1003            return Err(format!("Invalid version format: {}", self.project.version));
1004        }
1005
1006        // Validate workspace members if present
1007        if let Some(ref workspace) = self.workspace {
1008            if workspace.members.is_empty() {
1009                return Err("Workspace members cannot be empty".to_string());
1010            }
1011        }
1012
1013        // Validate minimum Rust version if specified
1014        if let Some(ref validation) = self.validation {
1015            if let Some(ref min_version) = validation.min_rust_version {
1016                if !self.is_valid_version(min_version) {
1017                    return Err(format!("Invalid min_rust_version: {}", min_version));
1018                }
1019            }
1020        }
1021
1022        self.validate_diataxis()?;
1023
1024        Ok(())
1025    }
1026
1027    fn validate_diataxis(&self) -> Result<(), String> {
1028        let Some(diataxis) = &self.diataxis else {
1029            return Ok(());
1030        };
1031
1032        if diataxis.root.trim().is_empty() {
1033            return Err("Diataxis root cannot be empty".to_string());
1034        }
1035
1036        if diataxis.index.trim().is_empty() {
1037            return Err("Diataxis index cannot be empty".to_string());
1038        }
1039
1040        let quadrants = diataxis
1041            .quadrants
1042            .as_ref()
1043            .ok_or_else(|| "Diataxis quadrants must be specified".to_string())?;
1044
1045        let mut seen = false;
1046        let mut check_section = |name: &str, section: &DiataxisSection| -> Result<(), String> {
1047            seen = true;
1048
1049            if let Some(source) = &section.source {
1050                if source.trim().is_empty() {
1051                    return Err(format!("{name} source cannot be empty"));
1052                }
1053            }
1054
1055            if let Some(output) = &section.output {
1056                if output.trim().is_empty() {
1057                    return Err(format!("{name} output cannot be empty"));
1058                }
1059            }
1060
1061            for nav in &section.navigation {
1062                if nav.title.trim().is_empty() {
1063                    return Err(format!("{name} navigation title cannot be empty"));
1064                }
1065                if nav.path.trim().is_empty() {
1066                    return Err(format!("{name} navigation path cannot be empty"));
1067                }
1068            }
1069
1070            Ok(())
1071        };
1072
1073        if let Some(section) = &quadrants.tutorials {
1074            check_section("tutorials", section)?;
1075        }
1076        if let Some(section) = &quadrants.how_to {
1077            check_section("how-to", section)?;
1078        }
1079        if let Some(section) = &quadrants.reference {
1080            check_section("reference", section)?;
1081        }
1082        if let Some(section) = &quadrants.explanations {
1083            check_section("explanations", section)?;
1084        }
1085
1086        if !seen {
1087            return Err("At least one diataxis quadrant must be defined".to_string());
1088        }
1089
1090        Ok(())
1091    }
1092
1093    /// Check if version string is valid (basic semver check)
1094    fn is_valid_version(&self, version: &str) -> bool {
1095        let parts: Vec<&str> = version.split('.').collect();
1096        parts.len() >= 2 && parts.iter().all(|p| p.parse::<u32>().is_ok())
1097    }
1098
1099    /// Get effective dependencies for a specific profile
1100    ///
1101    /// Merges base dependencies with profile-specific overrides
1102    pub fn get_profile_dependencies(&self, profile_name: &str) -> BTreeMap<String, Dependency> {
1103        let mut deps = self.dependencies.clone();
1104
1105        if let Some(ref profiles) = self.profiles {
1106            let profile = match profile_name {
1107                "dev" => profiles.dev.as_ref(),
1108                "production" => profiles.production.as_ref(),
1109                "test" => profiles.test.as_ref(),
1110                "ci" => profiles.ci.as_ref(),
1111                "bench" => profiles.bench.as_ref(),
1112                _ => None,
1113            };
1114
1115            if let Some(profile) = profile {
1116                if let Some(ref profile_deps) = profile.dependencies {
1117                    deps.extend(profile_deps.clone());
1118                }
1119            }
1120        }
1121
1122        deps
1123    }
1124
1125    /// Get template variables for a specific profile
1126    pub fn get_profile_template_vars(&self, profile_name: &str) -> BTreeMap<String, String> {
1127        let mut vars = self.vars.clone();
1128
1129        // Add template vars if present
1130        if let Some(ref templates) = self.templates {
1131            if let Some(ref template_vars) = templates.vars {
1132                vars.extend(template_vars.clone());
1133            }
1134        }
1135
1136        // Add profile-specific vars
1137        if let Some(ref profiles) = self.profiles {
1138            let profile = match profile_name {
1139                "dev" => profiles.dev.as_ref(),
1140                "production" => profiles.production.as_ref(),
1141                "test" => profiles.test.as_ref(),
1142                "ci" => profiles.ci.as_ref(),
1143                "bench" => profiles.bench.as_ref(),
1144                _ => None,
1145            };
1146
1147            if let Some(profile) = profile {
1148                if let Some(ref templates) = profile.templates {
1149                    if let Some(ref profile_vars) = templates.vars {
1150                        vars.extend(profile_vars.clone());
1151                    }
1152                }
1153            }
1154        }
1155
1156        vars
1157    }
1158
1159    /// Resolve diataxis sections with effective source/output paths and navigation
1160    ///
1161    /// Defaults:
1162    /// - source: `<root>/<quadrant>`
1163    /// - output: `<root>/generated/<quadrant>`
1164    pub fn resolved_diataxis_sections(&self) -> Option<Vec<ResolvedDiataxisSection>> {
1165        let diataxis = self.diataxis.as_ref()?;
1166        let quadrants = diataxis.quadrants.as_ref()?;
1167
1168        let mut sections = Vec::new();
1169
1170        let resolve =
1171            |quadrant: DiataxisQuadrant, section: &DiataxisSection| -> ResolvedDiataxisSection {
1172                let source = section
1173                    .source
1174                    .clone()
1175                    .unwrap_or_else(|| format!("{}/{}", diataxis.root, quadrant.as_dir()));
1176                let output = section.output.clone().unwrap_or_else(|| {
1177                    format!("{}/generated/{}", diataxis.root, quadrant.as_dir())
1178                });
1179
1180                ResolvedDiataxisSection {
1181                    quadrant,
1182                    source,
1183                    output,
1184                    navigation: section.navigation.clone(),
1185                }
1186            };
1187
1188        if let Some(section) = &quadrants.tutorials {
1189            sections.push(resolve(DiataxisQuadrant::Tutorials, section));
1190        }
1191        if let Some(section) = &quadrants.how_to {
1192            sections.push(resolve(DiataxisQuadrant::HowTo, section));
1193        }
1194        if let Some(section) = &quadrants.reference {
1195            sections.push(resolve(DiataxisQuadrant::Reference, section));
1196        }
1197        if let Some(section) = &quadrants.explanations {
1198            sections.push(resolve(DiataxisQuadrant::Explanations, section));
1199        }
1200
1201        if sections.is_empty() {
1202            None
1203        } else {
1204            Some(sections)
1205        }
1206    }
1207}
1208
1209impl Default for GgenConfig {
1210    fn default() -> Self {
1211        Self {
1212            project: Project {
1213                name: "untitled".to_string(),
1214                version: "0.1.0".to_string(),
1215                description: None,
1216                authors: Vec::new(),
1217                license: None,
1218                edition: Some("2021".to_string()),
1219                project_type: Some("auto".to_string()),
1220                language: Some("auto".to_string()),
1221                uri: None,
1222                namespace: None,
1223                extends: None,
1224                output_dir: None,
1225            },
1226            workspace: None,
1227            graph: None,
1228            dependencies: BTreeMap::new(),
1229            dev_dependencies: BTreeMap::new(),
1230            build_dependencies: BTreeMap::new(),
1231            target: BTreeMap::new(),
1232            ontology: None,
1233            templates: None,
1234            diataxis: None,
1235            generators: None,
1236            lifecycle: None,
1237            plugins: None,
1238            profiles: None,
1239            metadata: None,
1240            validation: None,
1241            prefixes: BTreeMap::new(),
1242            rdf: None,
1243            vars: BTreeMap::new(),
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250    use super::*;
1251
1252    #[test]
1253    fn test_default_config_is_valid() {
1254        let config = GgenConfig::default();
1255        assert!(config.validate().is_ok());
1256    }
1257
1258    #[test]
1259    fn test_version_validation() {
1260        let config = GgenConfig::default();
1261        assert!(config.is_valid_version("1.0.0"));
1262        assert!(config.is_valid_version("0.1.0"));
1263        assert!(config.is_valid_version("2.3.4"));
1264        assert!(!config.is_valid_version("invalid"));
1265        assert!(!config.is_valid_version("1"));
1266    }
1267
1268    #[test]
1269    fn test_empty_project_name_fails_validation() {
1270        let mut config = GgenConfig::default();
1271        config.project.name = String::new();
1272        assert!(config.validate().is_err());
1273    }
1274
1275    #[test]
1276    fn test_invalid_version_fails_validation() {
1277        let mut config = GgenConfig::default();
1278        config.project.version = "invalid".to_string();
1279        assert!(config.validate().is_err());
1280    }
1281
1282    #[test]
1283    fn test_profile_dependencies_merge() {
1284        let mut config = GgenConfig::default();
1285
1286        config
1287            .dependencies
1288            .insert("base".to_string(), Dependency::Simple("1.0".to_string()));
1289
1290        let mut dev_deps = BTreeMap::new();
1291        dev_deps.insert("dev-dep".to_string(), Dependency::Simple("2.0".to_string()));
1292
1293        config.profiles = Some(Profiles {
1294            default: Some("dev".to_string()),
1295            dev: Some(Profile {
1296                extends: None,
1297                optimization: None,
1298                debug_assertions: None,
1299                overflow_checks: None,
1300                lto: None,
1301                strip: None,
1302                codegen_units: None,
1303                code_coverage: None,
1304                test_threads: None,
1305                dependencies: Some(dev_deps),
1306                templates: None,
1307                ontology: None,
1308                lifecycle: None,
1309            }),
1310            production: None,
1311            test: None,
1312            ci: None,
1313            bench: None,
1314        });
1315
1316        let deps = config.get_profile_dependencies("dev");
1317        assert_eq!(deps.len(), 2);
1318        assert!(deps.contains_key("base"));
1319        assert!(deps.contains_key("dev-dep"));
1320    }
1321
1322    #[test]
1323    fn test_resolved_diataxis_sections_with_defaults() {
1324        let config: GgenConfig = toml::from_str(
1325            r#"
1326            [project]
1327            name = "docs"
1328            version = "1.0.0"
1329
1330            [diataxis]
1331
1332            [diataxis.quadrants.tutorials]
1333            output = "generated/tutorials"
1334
1335            [[diataxis.quadrants.tutorials.navigation]]
1336            title = "Intro"
1337            path = "diataxis/tutorials/intro.md"
1338        "#,
1339        )
1340        .unwrap();
1341
1342        config.validate().unwrap();
1343        let sections = config.resolved_diataxis_sections().unwrap();
1344        let tutorials = sections
1345            .iter()
1346            .find(|s| matches!(s.quadrant, DiataxisQuadrant::Tutorials))
1347            .unwrap();
1348
1349        assert_eq!(tutorials.source, "docs/tutorials");
1350        assert_eq!(tutorials.output, "generated/tutorials");
1351        assert_eq!(tutorials.navigation.len(), 1);
1352    }
1353
1354    #[test]
1355    fn test_invalid_diataxis_empty_nav_path() {
1356        let config: GgenConfig = toml::from_str(
1357            r#"
1358            [project]
1359            name = "docs"
1360            version = "1.0.0"
1361
1362            [diataxis]
1363
1364            [diataxis.quadrants.reference]
1365            source = "docs/reference"
1366
1367            [[diataxis.quadrants.reference.navigation]]
1368            title = "Ref"
1369            path = ""
1370        "#,
1371        )
1372        .unwrap();
1373
1374        assert!(config.validate().is_err());
1375    }
1376}