1#![allow(clippy::unused_unit)] use clap_noun_verb_macros::verb;
9use serde::Serialize;
10use std::fs;
11use std::path::Path;
12
13#[derive(Debug, Clone, Serialize)]
19pub struct InitOutput {
20 pub status: String,
22
23 pub project_dir: String,
25
26 pub files_created: Vec<String>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub files_overwritten: Option<Vec<String>>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub files_preserved: Option<Vec<String>>,
36
37 pub directories_created: Vec<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub error: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub warning: Option<String>,
47
48 pub next_steps: Vec<String>,
50}
51
52const GGEN_TOML: &str = r#"[project]
57name = "my-ggen-project"
58version = "0.1.0"
59description = "A ggen project initialized with default templates"
60authors = ["ggen init"]
61license = "MIT"
62
63[generation]
64ontology_dir = "schema/"
65templates_dir = "templates/"
66output_dir = "src/generated/"
67incremental = true
68overwrite = false
69
70[sync]
71enabled = true
72on_change = "manual"
73validate_after = true
74conflict_mode = "fail"
75
76[rdf]
77formats = ["turtle"]
78default_format = "turtle"
79strict_validation = false
80
81[templates]
82enable_caching = true
83auto_reload = true
84
85[output]
86formatting = "default"
87line_length = 100
88indent = 2
89"#;
90
91const DOMAIN_TTL: &str = r#"@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
92@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
93@prefix ex: <https://example.com/> .
94
95# Example Person class
96ex:Person a rdfs:Class ;
97 rdfs:label "Person" ;
98 rdfs:comment "Represents a person in the system" .
99
100# Example properties for Person
101ex:name a rdf:Property ;
102 rdfs:domain ex:Person ;
103 rdfs:range rdfs:Literal ;
104 rdfs:label "name" ;
105 rdfs:comment "The name of the person" .
106
107ex:email a rdf:Property ;
108 rdfs:domain ex:Person ;
109 rdfs:range rdfs:Literal ;
110 rdfs:label "email" ;
111 rdfs:comment "The email address of the person" .
112
113ex:age a rdf:Property ;
114 rdfs:domain ex:Person ;
115 rdfs:range rdfs:Literal ;
116 rdfs:label "age" ;
117 rdfs:comment "The age of the person" .
118"#;
119
120const MAKEFILE: &str = r#".PHONY: help setup build clean
121
122help:
123 @echo "ggen project - Available targets:"
124 @echo " make setup - Run startup.sh to initialize project"
125 @echo " make build - Generate code from ontology (ggen sync)"
126 @echo " make clean - Remove generated artifacts"
127 @echo ""
128 @echo "See scripts/startup.sh for custom initialization steps"
129
130setup:
131 @bash scripts/startup.sh
132
133build:
134 ggen sync
135
136clean:
137 rm -rf src/generated/
138 rm -rf .ggen/
139"#;
140
141const STARTUP_SH: &str = r#"#!/bin/bash
142# Startup script for ggen project initialization
143# Run this to prepare your environment for development
144
145set -e
146
147echo "🚀 Initializing ggen project..."
148
149# Create directories if they don't exist
150mkdir -p schema
151mkdir -p templates
152mkdir -p src/generated
153mkdir -p scripts
154
155echo "✅ Project structure created"
156
157# Display next steps
158echo ""
159echo "📋 Next steps:"
160echo " 1. Edit schema/domain.ttl to define your domain model"
161echo " 2. Create Tera templates in templates/ for your target languages"
162echo " 3. Run 'ggen sync' to generate code from your ontology"
163echo " 4. Run 'make build' to regenerate after changes"
164echo ""
165echo "📚 Learn more:"
166echo " - ggen Documentation: https://docs.ggen.io"
167echo " - RDF/Turtle Syntax: https://www.w3.org/TR/turtle/"
168echo " - Tera Template Language: https://keats.github.io/tera/"
169"#;
170
171#[allow(clippy::unused_unit)]
213#[verb("init", "root")]
214pub fn init(path: Option<String>, force: Option<bool>) -> clap_noun_verb::Result<InitOutput> {
215 let project_dir = path.unwrap_or_else(|| ".".to_string());
217 let force = force.unwrap_or(false);
218
219 perform_init(&project_dir, force)
221}
222
223fn perform_init(
234 project_dir: &str,
235 force: bool,
236) -> clap_noun_verb::Result<InitOutput> {
237 let base_path = Path::new(project_dir);
239
240 let ggen_artifacts = vec!["ggen.toml", "Makefile", "schema/domain.ttl", "scripts/startup.sh"];
242
243 let has_ggen_artifacts = ggen_artifacts.iter().any(|artifact| {
245 let path = base_path.join(artifact);
246 path.exists()
247 });
248
249 if has_ggen_artifacts && !force {
250 return Ok(InitOutput {
251 status: "error".to_string(),
252 project_dir: project_dir.to_string(),
253 files_created: vec![],
254 files_overwritten: None,
255 files_preserved: None,
256 directories_created: vec![],
257 error: Some(
258 "ggen project already initialized here. Use --force to reinitialize.".to_string(),
259 ),
260 warning: None,
261 next_steps: vec!["Run 'make build' to regenerate code".to_string()],
262 });
263 }
264
265 if let Err(e) = fs::create_dir_all(base_path) {
267 return Ok(InitOutput {
268 status: "error".to_string(),
269 project_dir: project_dir.to_string(),
270 files_created: vec![],
271 files_overwritten: None,
272 files_preserved: None,
273 directories_created: vec![],
274 error: Some(format!("Failed to create project directory: {}", e)),
275 warning: None,
276 next_steps: vec![],
277 });
278 }
279
280 let temp_test = base_path.join(".ggen_write_test");
282 match fs::write(&temp_test, "") {
283 Ok(_) => {
284 let _ = fs::remove_file(&temp_test); }
286 Err(e) => {
287 return Ok(InitOutput {
288 status: "error".to_string(),
289 project_dir: project_dir.to_string(),
290 files_created: vec![],
291 files_overwritten: None,
292 files_preserved: None,
293 directories_created: vec![],
294 error: Some(format!(
295 "No write permission in project directory: {}",
296 e
297 )),
298 warning: None,
299 next_steps: vec!["Check directory permissions or try a different location"
300 .to_string()],
301 });
302 }
303 }
304
305 let mut files_created = vec![];
306 let mut files_overwritten = vec![];
307 let mut files_preserved = vec![];
308 let mut directories_created = vec![];
309 let mut errors = vec![];
310
311 let dirs = vec!["schema", "templates", "src/generated", "scripts"];
313 for dir in &dirs {
314 let dir_path = base_path.join(dir);
315 let existed = dir_path.exists();
316 match fs::create_dir_all(&dir_path) {
317 Ok(_) => {
318 if !existed {
319 directories_created.push(dir.to_string());
320 }
321 }
322 Err(e) => {
323 errors.push(format!("Failed to create directory {}: {}", dir, e));
324 }
325 }
326 }
327
328 let mut write_file = |path: &Path, content: &str, filename: &str| {
330 let exists = path.exists();
331 match fs::write(path, content) {
332 Ok(_) => {
333 if exists {
334 files_overwritten.push(filename.to_string());
335 } else {
336 files_created.push(filename.to_string());
337 }
338 }
339 Err(e) => errors.push(format!("Failed to write {}: {}", filename, e)),
340 }
341 };
342
343 let toml_path = base_path.join("ggen.toml");
345 write_file(&toml_path, GGEN_TOML, "ggen.toml");
346
347 let schema_path = base_path.join("schema").join("domain.ttl");
349 write_file(&schema_path, DOMAIN_TTL, "schema/domain.ttl");
350
351 let makefile_path = base_path.join("Makefile");
353 write_file(&makefile_path, MAKEFILE, "Makefile");
354
355 let startup_sh_path = base_path.join("scripts").join("startup.sh");
357 write_file(&startup_sh_path, STARTUP_SH, "scripts/startup.sh");
358
359 let gitignore_path = base_path.join(".gitignore");
361 if !gitignore_path.exists() {
362 let gitignore_content = "# ggen outputs\nsrc/generated/\n.ggen/\n";
363 write_file(&gitignore_path, gitignore_content, ".gitignore");
364 } else {
365 files_preserved.push(".gitignore".to_string());
366 }
367
368 let readme_path = base_path.join("README.md");
370 let readme_content = r#"# My ggen Project
371
372Generated by `ggen init`.
373
374## Getting Started
375
3761. **Initialize project**: Run `make setup` to prepare your environment
3772. **Define schema**: Edit `schema/domain.ttl` with your domain model
3783. **Create templates**: Add Tera templates in `templates/` for your target languages
3794. **Generate code**: Run `make build` to generate code from your ontology
380
381## Project Structure
382
383```
384.
385├── Makefile # Build targets (setup, build, clean)
386├── ggen.toml # ggen project configuration
387├── README.md # This file
388├── schema/
389│ └── domain.ttl # RDF ontology (Turtle format)
390├── templates/ # Tera templates for code generation
391│ └── (create your templates here)
392├── scripts/
393│ └── startup.sh # Project initialization script
394└── src/
395 └── generated/ # Generated code output
396```
397
398## Makefile Targets
399
400```bash
401make help # Show available targets
402make setup # Run scripts/startup.sh to initialize project
403make build # Generate code from ontology (ggen sync)
404make clean # Remove generated artifacts
405```
406
407## Manual Commands
408
409```bash
410# Generate code from ontology
411ggen sync
412
413# Dry-run: preview changes without writing
414ggen sync --dry-run
415
416# Watch mode: regenerate on file changes
417ggen sync --watch
418
419# Validate without generating
420ggen sync --validate-only
421```
422
423## Adding Templates
424
425Create Tera templates in the `templates/` directory. Each template can generate code in your target language.
426
427Example template structure:
428```
429templates/
430├── rust.tera # Rust code generator
431├── typescript.tera # TypeScript code generator
432└── python.tera # Python code generator
433```
434
435See `ggen.toml` to configure which templates to run and where they output.
436
437## Learn More
438
439- [ggen Documentation](https://docs.ggen.io)
440- [RDF/Turtle Syntax](https://www.w3.org/TR/turtle/)
441- [Tera Template Language](https://keats.github.io/tera/)
442- [Custom Startup Scripts](scripts/startup.sh)
443"#;
444 if !readme_path.exists() {
446 write_file(&readme_path, readme_content, "README.md");
447 } else {
448 files_preserved.push("README.md".to_string());
449 }
450
451 drop(write_file);
453
454 #[cfg(unix)]
457 {
458 use std::os::unix::fs::PermissionsExt;
459 if let Err(e) = fs::set_permissions(
460 &startup_sh_path,
461 std::fs::Permissions::from_mode(0o755),
462 ) {
463 errors.push(format!(
464 "Warning: Could not set execute permissions on startup.sh: {}",
465 e
466 ));
467 }
468 }
469
470 let status = if errors.is_empty() {
472 "success".to_string()
473 } else {
474 "partial".to_string()
475 };
476
477 let error = if errors.is_empty() {
478 None
479 } else {
480 Some(errors.join("; "))
481 };
482
483 let warning = if !files_overwritten.is_empty() || !files_preserved.is_empty() {
485 let mut msgs = vec![];
486 if !files_overwritten.is_empty() {
487 msgs.push(format!("Overwrote {} file(s)", files_overwritten.len()));
488 }
489 if !files_preserved.is_empty() {
490 msgs.push(format!("Preserved {} user file(s)", files_preserved.len()));
491 }
492 Some(msgs.join("; ") + ".")
493 } else {
494 None
495 };
496
497 let files_overwritten_opt = if files_overwritten.is_empty() {
498 None
499 } else {
500 Some(files_overwritten)
501 };
502
503 let files_preserved_opt = if files_preserved.is_empty() {
504 None
505 } else {
506 Some(files_preserved)
507 };
508
509 Ok(InitOutput {
510 status,
511 project_dir: project_dir.to_string(),
512 files_created,
513 files_overwritten: files_overwritten_opt,
514 files_preserved: files_preserved_opt,
515 directories_created,
516 error,
517 warning,
518 next_steps: vec![
519 "Run 'make setup' to initialize your project".to_string(),
520 "Edit schema/domain.ttl to define your domain model".to_string(),
521 "Create Tera templates in templates/ for your target languages".to_string(),
522 "Run 'make build' to generate code from your ontology".to_string(),
523 ],
524 })
525}