1use serde::{Deserialize, Serialize};
64use std::collections::BTreeMap;
65use std::path::PathBuf;
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "kebab-case")]
74pub struct Diataxis {
75 #[serde(default = "default_diataxis_root")]
77 pub root: String,
78
79 #[serde(default = "default_diataxis_index")]
81 pub index: String,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub quadrants: Option<DiataxisQuadrants>,
86}
87
88#[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#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "kebab-case")]
108pub struct DiataxisSection {
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub source: Option<String>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub output: Option<String>,
116
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub navigation: Vec<DiataxisNav>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124#[serde(rename_all = "kebab-case")]
125pub struct DiataxisNav {
126 pub title: String,
128
129 pub path: String,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub description: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "kebab-case")]
147pub struct GgenConfig {
148 pub project: Project,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub workspace: Option<Workspace>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub graph: Option<Graph>,
158
159 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
161 pub dependencies: BTreeMap<String, Dependency>,
162
163 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
165 pub dev_dependencies: BTreeMap<String, Dependency>,
166
167 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
169 pub build_dependencies: BTreeMap<String, Dependency>,
170
171 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
173 pub target: BTreeMap<String, TargetDependencies>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub ontology: Option<Ontology>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub templates: Option<Templates>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub diataxis: Option<Diataxis>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub generators: Option<Generators>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub lifecycle: Option<Lifecycle>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub plugins: Option<Plugins>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub profiles: Option<Profiles>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub metadata: Option<Metadata>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub validation: Option<Validation>,
210
211 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
213 pub prefixes: BTreeMap<String, String>,
214
215 #[serde(rename = "rdf", skip_serializing_if = "Option::is_none")]
217 pub rdf: Option<RdfConfig>,
218
219 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
221 pub vars: BTreeMap<String, String>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
230#[serde(rename_all = "kebab-case")]
231pub struct Project {
232 pub name: String,
234
235 pub version: String,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub description: Option<String>,
241
242 #[serde(default, skip_serializing_if = "Vec::is_empty")]
244 pub authors: Vec<String>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub license: Option<String>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub edition: Option<String>,
253
254 #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
256 pub project_type: Option<String>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub language: Option<String>,
261
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub uri: Option<String>,
265
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub namespace: Option<String>,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub extends: Option<String>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub output_dir: Option<PathBuf>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "kebab-case")]
286pub struct Workspace {
287 #[serde(default)]
289 pub members: Vec<String>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub exclude: Option<Vec<String>>,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub dependencies: Option<BTreeMap<String, Dependency>>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub graph: Option<WorkspaceGraph>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct WorkspaceGraph {
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub query: Option<String>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "kebab-case")]
319pub struct Graph {
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub strategy: Option<String>,
323
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub conflict_resolution: Option<String>,
327
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub queries: Option<BTreeMap<String, String>>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub features: Option<BTreeMap<String, Vec<String>>>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(untagged)]
344pub enum Dependency {
345 Simple(String),
347 Detailed(DetailedDependency),
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "kebab-case")]
354pub struct DetailedDependency {
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub version: Option<String>,
358
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub features: Vec<String>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub optional: Option<bool>,
366
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub default_features: Option<bool>,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub git: Option<String>,
374
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub branch: Option<String>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub tag: Option<String>,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub rev: Option<String>,
386
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub path: Option<PathBuf>,
390
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub registry: Option<String>,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub package: Option<String>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct TargetDependencies {
403 #[serde(default)]
405 pub dependencies: BTreeMap<String, Dependency>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
414#[serde(rename_all = "kebab-case")]
415pub struct Ontology {
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub files: Vec<PathBuf>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub inline: Option<String>,
423
424 #[serde(default, skip_serializing_if = "Vec::is_empty")]
426 pub shapes: Vec<PathBuf>,
427
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub constitution: Option<Constitution>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct Constitution {
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub checks: Vec<String>,
439
440 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
442 pub custom: BTreeMap<String, String>,
443
444 #[serde(skip_serializing_if = "Option::is_none")]
446 pub enforce_strict: Option<bool>,
447
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub fail_on_warning: Option<bool>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
459#[serde(rename_all = "kebab-case")]
460pub struct Templates {
461 #[serde(default, skip_serializing_if = "Vec::is_empty")]
463 pub paths: Vec<String>,
464
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub vars: Option<BTreeMap<String, String>>,
468
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub extends: Option<BTreeMap<String, String>>,
472
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub compose: Option<BTreeMap<String, Vec<String>>>,
476
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub guards: Option<BTreeMap<String, String>>,
480
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub queries: Option<BTreeMap<String, String>>,
484}
485
486#[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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(rename_all = "kebab-case")]
532pub struct Generators {
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub registry: Option<String>,
536
537 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
539 pub installed: BTreeMap<String, InstalledGenerator>,
540
541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
543 pub pipeline: Vec<GeneratorPipeline>,
544
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub hooks: Option<GeneratorHooks>,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct InstalledGenerator {
553 pub version: String,
555
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub registry: Option<String>,
559
560 #[serde(skip_serializing_if = "Option::is_none")]
562 pub source: Option<String>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct GeneratorPipeline {
568 pub name: String,
570
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub description: Option<String>,
574
575 #[serde(default, skip_serializing_if = "Vec::is_empty")]
577 pub inputs: Vec<String>,
578
579 #[serde(default)]
581 pub steps: Vec<PipelineStep>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct PipelineStep {
587 pub action: String,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub template: Option<String>,
593
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub parser: Option<String>,
597
598 #[serde(skip_serializing_if = "Option::is_none")]
600 pub query: Option<String>,
601
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub command: Option<String>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct GeneratorHooks {
610 #[serde(skip_serializing_if = "Option::is_none")]
612 pub before_generate: Option<String>,
613
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub after_generate: Option<String>,
617
618 #[serde(skip_serializing_if = "Option::is_none")]
620 pub on_error: Option<String>,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "kebab-case")]
630pub struct Lifecycle {
631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
633 pub phases: Vec<String>,
634
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub hooks: Option<BTreeMap<String, Vec<String>>>,
638
639 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
641 pub tasks: BTreeMap<String, LifecycleTask>,
642
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub parallel: Option<BTreeMap<String, Vec<String>>>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct LifecycleTask {
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub description: Option<String>,
654
655 #[serde(default, skip_serializing_if = "Vec::is_empty")]
657 pub dependencies: Vec<String>,
658
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub command: Option<String>,
662
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub script: Option<String>,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
674#[serde(rename_all = "kebab-case")]
675pub struct Plugins {
676 #[serde(default, skip_serializing_if = "Vec::is_empty")]
678 pub paths: Vec<String>,
679
680 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
682 pub installed: BTreeMap<String, InstalledPlugin>,
683
684 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
686 pub config: BTreeMap<String, BTreeMap<String, toml::Value>>,
687
688 #[serde(skip_serializing_if = "Option::is_none")]
690 pub hooks: Option<BTreeMap<String, Vec<String>>>,
691
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub permissions: Option<BTreeMap<String, PluginPermissions>>,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct InstalledPlugin {
700 pub version: String,
702
703 pub source: String,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct PluginPermissions {
710 #[serde(default, skip_serializing_if = "Vec::is_empty")]
712 pub filesystem: Vec<String>,
713
714 #[serde(default, skip_serializing_if = "Vec::is_empty")]
716 pub network: Vec<String>,
717
718 #[serde(default, skip_serializing_if = "Vec::is_empty")]
720 pub exec: Vec<String>,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
729#[serde(rename_all = "kebab-case")]
730pub struct Profiles {
731 #[serde(skip_serializing_if = "Option::is_none")]
733 pub default: Option<String>,
734
735 #[serde(skip_serializing_if = "Option::is_none")]
737 pub dev: Option<Profile>,
738
739 #[serde(skip_serializing_if = "Option::is_none")]
741 pub production: Option<Profile>,
742
743 #[serde(skip_serializing_if = "Option::is_none")]
745 pub test: Option<Profile>,
746
747 #[serde(skip_serializing_if = "Option::is_none")]
749 pub ci: Option<Profile>,
750
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub bench: Option<Profile>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "kebab-case")]
759pub struct Profile {
760 #[serde(skip_serializing_if = "Option::is_none")]
762 pub extends: Option<String>,
763
764 #[serde(skip_serializing_if = "Option::is_none")]
766 pub optimization: Option<String>,
767
768 #[serde(skip_serializing_if = "Option::is_none")]
770 pub debug_assertions: Option<bool>,
771
772 #[serde(skip_serializing_if = "Option::is_none")]
774 pub overflow_checks: Option<bool>,
775
776 #[serde(skip_serializing_if = "Option::is_none")]
778 pub lto: Option<String>,
779
780 #[serde(skip_serializing_if = "Option::is_none")]
782 pub strip: Option<bool>,
783
784 #[serde(skip_serializing_if = "Option::is_none")]
786 pub codegen_units: Option<u32>,
787
788 #[serde(skip_serializing_if = "Option::is_none")]
790 pub code_coverage: Option<bool>,
791
792 #[serde(skip_serializing_if = "Option::is_none")]
794 pub test_threads: Option<u32>,
795
796 #[serde(skip_serializing_if = "Option::is_none")]
798 pub dependencies: Option<BTreeMap<String, Dependency>>,
799
800 #[serde(skip_serializing_if = "Option::is_none")]
802 pub templates: Option<ProfileTemplates>,
803
804 #[serde(skip_serializing_if = "Option::is_none")]
806 pub ontology: Option<ProfileOntology>,
807
808 #[serde(skip_serializing_if = "Option::is_none")]
810 pub lifecycle: Option<ProfileLifecycle>,
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct ProfileTemplates {
816 #[serde(skip_serializing_if = "Option::is_none")]
818 pub vars: Option<BTreeMap<String, String>>,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct ProfileOntology {
824 #[serde(skip_serializing_if = "Option::is_none")]
826 pub constitution: Option<Constitution>,
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct ProfileLifecycle {
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub hooks: Option<BTreeMap<String, Vec<String>>>,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize)]
843#[serde(rename_all = "kebab-case")]
844pub struct Metadata {
845 #[serde(skip_serializing_if = "Option::is_none")]
847 pub homepage: Option<String>,
848
849 #[serde(skip_serializing_if = "Option::is_none")]
851 pub documentation: Option<String>,
852
853 #[serde(skip_serializing_if = "Option::is_none")]
855 pub repository: Option<String>,
856
857 #[serde(skip_serializing_if = "Option::is_none")]
859 pub changelog: Option<String>,
860
861 #[serde(skip_serializing_if = "Option::is_none")]
863 pub build: Option<BuildMetadata>,
864
865 #[serde(skip_serializing_if = "Option::is_none")]
867 pub package: Option<PackageMetadata>,
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct BuildMetadata {
873 #[serde(skip_serializing_if = "Option::is_none")]
875 pub rustc_version: Option<String>,
876
877 #[serde(skip_serializing_if = "Option::is_none")]
879 pub llvm_version: Option<String>,
880
881 #[serde(skip_serializing_if = "Option::is_none")]
883 pub target_triple: Option<String>,
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct PackageMetadata {
889 #[serde(default, skip_serializing_if = "Vec::is_empty")]
891 pub categories: Vec<String>,
892
893 #[serde(default, skip_serializing_if = "Vec::is_empty")]
895 pub keywords: Vec<String>,
896
897 #[serde(skip_serializing_if = "Option::is_none")]
899 pub readme: Option<String>,
900
901 #[serde(default, skip_serializing_if = "Vec::is_empty")]
903 pub include: Vec<String>,
904
905 #[serde(default, skip_serializing_if = "Vec::is_empty")]
907 pub exclude: Vec<String>,
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize)]
916#[serde(rename_all = "kebab-case")]
917pub struct Validation {
918 #[serde(skip_serializing_if = "Option::is_none")]
920 pub min_rust_version: Option<String>,
921
922 #[serde(skip_serializing_if = "Option::is_none")]
924 pub dependencies: Option<DependencyValidation>,
925
926 #[serde(skip_serializing_if = "Option::is_none")]
928 pub quality: Option<QualityThresholds>,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct DependencyValidation {
934 #[serde(skip_serializing_if = "Option::is_none")]
936 pub max_duplicates: Option<u32>,
937
938 #[serde(skip_serializing_if = "Option::is_none")]
940 pub allow_yanked: Option<bool>,
941
942 #[serde(skip_serializing_if = "Option::is_none")]
944 pub require_checksums: Option<bool>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
949pub struct QualityThresholds {
950 #[serde(skip_serializing_if = "Option::is_none")]
952 pub min_coverage: Option<u32>,
953
954 #[serde(skip_serializing_if = "Option::is_none")]
956 pub max_complexity: Option<u32>,
957
958 #[serde(skip_serializing_if = "Option::is_none")]
960 pub max_function_lines: Option<u32>,
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize)]
969pub struct RdfConfig {
970 #[serde(default)]
972 pub files: Vec<PathBuf>,
973
974 #[serde(default)]
976 pub inline: Vec<String>,
977}
978
979impl GgenConfig {
984 pub fn validate(&self) -> Result<(), String> {
996 if self.project.name.is_empty() {
998 return Err("Project name cannot be empty".to_string());
999 }
1000
1001 if !self.is_valid_version(&self.project.version) {
1003 return Err(format!("Invalid version format: {}", self.project.version));
1004 }
1005
1006 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 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) = §ion.source {
1050 if source.trim().is_empty() {
1051 return Err(format!("{name} source cannot be empty"));
1052 }
1053 }
1054
1055 if let Some(output) = §ion.output {
1056 if output.trim().is_empty() {
1057 return Err(format!("{name} output cannot be empty"));
1058 }
1059 }
1060
1061 for nav in §ion.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 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 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 pub fn get_profile_template_vars(&self, profile_name: &str) -> BTreeMap<String, String> {
1127 let mut vars = self.vars.clone();
1128
1129 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 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 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}