lmrc_cli/generator/
mod.rs

1pub mod apps;
2pub mod documentation;
3pub mod gitlab_ci;
4pub mod pipeline;
5pub mod workspace;
6
7use colored::Colorize;
8use lmrc_config_validator::LmrcConfig;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use crate::error::Result;
13
14pub struct ProjectGenerator {
15    config: LmrcConfig,
16    project_path: PathBuf,
17}
18
19impl ProjectGenerator {
20    pub fn new(config: LmrcConfig, project_path: PathBuf) -> Self {
21        Self {
22            config,
23            project_path,
24        }
25    }
26
27    pub fn project_path(&self) -> &Path {
28        &self.project_path
29    }
30
31    pub async fn generate(&self) -> Result<()> {
32        println!("{}", "Generating project structure...".yellow());
33
34        // Create root directory
35        fs::create_dir_all(&self.project_path)?;
36
37        // Create directory structure
38        self.create_directory_structure()?;
39
40        // Generate configuration file
41        self.generate_config_file()?;
42
43        // Generate applications
44        self.generate_applications()?;
45
46        // Generate libs directory with placeholders
47        self.generate_libs()?;
48
49        // Generate pipeline application
50        pipeline::generate_pipeline_app(&self.project_path, &self.config)?;
51
52        // Generate workspace Cargo.toml AFTER apps and pipeline are created
53        workspace::generate_workspace_toml(&self.project_path, &self.config)?;
54
55        // Generate GitLab CI configuration
56        gitlab_ci::generate_gitlab_ci(&self.project_path, &self.config)?;
57
58        // Generate documentation
59        documentation::generate_docs(&self.project_path, &self.config)?;
60
61        // Generate Docker files
62        self.generate_docker_files()?;
63
64        // Generate .gitignore
65        self.generate_gitignore()?;
66
67        Ok(())
68    }
69
70    pub async fn generate_minimal(&self) -> Result<()> {
71        println!("{}", "Generating minimal project structure...".yellow());
72
73        // Create root directory
74        fs::create_dir_all(&self.project_path)?;
75
76        // Create directory structure
77        self.create_directory_structure()?;
78
79        // Don't generate Cargo.toml for minimal projects - it will be created when first component is added
80
81        // Generate configuration file
82        self.generate_config_file()?;
83
84        // Generate libs directory with placeholders
85        self.generate_libs()?;
86
87        // Generate Docker files
88        self.generate_docker_files()?;
89
90        // Generate .gitignore
91        self.generate_gitignore()?;
92
93        Ok(())
94    }
95
96    fn create_directory_structure(&self) -> Result<()> {
97        let dirs = vec!["apps", "libs", "docs", "docker", "infra"];
98
99        for dir in dirs {
100            let path = self.project_path.join(dir);
101            fs::create_dir_all(&path)?;
102            println!("  {} {}", "Created:".green(), path.display());
103        }
104
105        Ok(())
106    }
107
108    fn generate_config_file(&self) -> Result<()> {
109        let config_content = self.config.to_toml_string()?;
110        let config_path = self.project_path.join("lmrc.toml");
111        fs::write(&config_path, config_content)?;
112        println!("  {} {}", "Created:".green(), config_path.display());
113        Ok(())
114    }
115
116    fn generate_applications(&self) -> Result<()> {
117        // Use the new apps module to generate applications from templates
118        apps::generate_applications(&self.project_path, &self.config)?;
119        Ok(())
120    }
121
122    fn generate_libs(&self) -> Result<()> {
123        let libs_path = self.project_path.join("libs");
124        let readme = r#"# Shared Libraries
125
126This directory contains shared libraries used across multiple applications.
127
128Place reusable code, utilities, and shared logic here.
129"#;
130        fs::write(libs_path.join("README.md"), readme)?;
131        println!("  {} libs/README.md", "Created:".green());
132        Ok(())
133    }
134
135    fn generate_docker_files(&self) -> Result<()> {
136        let docker_path = self.project_path.join("docker");
137
138        // Generate Dockerfile template for applications
139        let dockerfile = r#"# Build stage
140FROM rust:1.75-slim as builder
141
142WORKDIR /app
143COPY . .
144
145RUN cargo build --release
146
147# Runtime stage
148FROM debian:bookworm-slim
149
150RUN apt-get update && \
151    apt-get install -y ca-certificates && \
152    rm -rf /var/lib/apt/lists/*
153
154WORKDIR /app
155
156COPY --from=builder /app/target/release/app /app/app
157
158ENTRYPOINT ["/app/app"]
159"#;
160
161        fs::write(docker_path.join("Dockerfile"), dockerfile)?;
162        println!("  {} docker/Dockerfile", "Created:".green());
163
164        // Generate docker-compose.yml for local development with all configured services
165        let docker_compose = self.generate_docker_compose()?;
166        fs::write(docker_path.join("docker-compose.yml"), docker_compose)?;
167        println!("  {} docker/docker-compose.yml", "Created:".green());
168
169        Ok(())
170    }
171
172    fn generate_docker_compose(&self) -> Result<String> {
173        let mut services = String::new();
174        let mut volumes = String::new();
175
176        // Add PostgreSQL if configured
177        if self.config.infrastructure.postgres.is_some() {
178            let pg_version = self.config
179                .infrastructure
180                .postgres
181                .as_ref()
182                .map(|p| p.version.as_str())
183                .unwrap_or("16");
184            let pg_db = self.config
185                .infrastructure
186                .postgres
187                .as_ref()
188                .map(|p| p.database_name.as_str())
189                .unwrap_or("myapp");
190
191            services.push_str(&format!(
192                r#"  postgres:
193    image: postgres:{}
194    container_name: {}-postgres
195    environment:
196      POSTGRES_DB: {}
197      POSTGRES_USER: postgres
198      POSTGRES_PASSWORD: postgres
199    ports:
200      - "5432:5432"
201    volumes:
202      - postgres_data:/var/lib/postgresql/data
203    healthcheck:
204      test: ["CMD-SHELL", "pg_isready -U postgres"]
205      interval: 10s
206      timeout: 5s
207      retries: 5
208    networks:
209      - lmrc-network
210
211"#,
212                pg_version, self.config.project.name, pg_db
213            ));
214            volumes.push_str("  postgres_data:\n");
215        }
216
217        // Add RabbitMQ if configured
218        if self.config.infrastructure.rabbitmq.is_some() {
219            services.push_str(&format!(
220                r#"  rabbitmq:
221    image: rabbitmq:3-management
222    container_name: {}-rabbitmq
223    environment:
224      RABBITMQ_DEFAULT_USER: guest
225      RABBITMQ_DEFAULT_PASS: guest
226    ports:
227      - "5672:5672"   # AMQP port
228      - "15672:15672" # Management UI
229    volumes:
230      - rabbitmq_data:/var/lib/rabbitmq
231    healthcheck:
232      test: ["CMD", "rabbitmq-diagnostics", "ping"]
233      interval: 10s
234      timeout: 5s
235      retries: 5
236    networks:
237      - lmrc-network
238
239"#,
240                self.config.project.name
241            ));
242            volumes.push_str("  rabbitmq_data:\n");
243        }
244
245        // Add Vault if configured
246        if self.config.infrastructure.vault.is_some() {
247            services.push_str(&format!(
248                r#"  vault:
249    image: hashicorp/vault:latest
250    container_name: {}-vault
251    environment:
252      VAULT_DEV_ROOT_TOKEN_ID: root
253      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
254    ports:
255      - "8200:8200"
256    cap_add:
257      - IPC_LOCK
258    networks:
259      - lmrc-network
260    command: server -dev
261
262"#,
263                self.config.project.name
264            ));
265        }
266
267        // Add Redis (common for caching, always include)
268        services.push_str(&format!(
269            r#"  redis:
270    image: redis:7-alpine
271    container_name: {}-redis
272    ports:
273      - "6379:6379"
274    volumes:
275      - redis_data:/data
276    healthcheck:
277      test: ["CMD", "redis-cli", "ping"]
278      interval: 10s
279      timeout: 5s
280      retries: 5
281    networks:
282      - lmrc-network
283
284"#,
285            self.config.project.name
286        ));
287        volumes.push_str("  redis_data:\n");
288
289        // Build complete docker-compose.yml
290        let mut compose = String::from("version: '3.8'\n\nservices:\n");
291        compose.push_str(&services);
292
293        if !volumes.is_empty() {
294            compose.push_str("\nvolumes:\n");
295            compose.push_str(&volumes);
296        }
297
298        compose.push_str(
299            r#"
300networks:
301  lmrc-network:
302    driver: bridge
303"#,
304        );
305
306        Ok(compose)
307    }
308
309    fn generate_gitignore(&self) -> Result<()> {
310        let gitignore = r#"# Rust
311/target/
312**/*.rs.bk
313*.pdb
314Cargo.lock
315
316# IDE
317.idea/
318.vscode/
319*.swp
320*.swo
321*~
322
323# OS
324.DS_Store
325Thumbs.db
326
327# Environment
328.env
329.env.local
330
331# Secrets
332secrets/
333.secrets/
334*.pem
335*.key
336
337# SSH Keys (project-local)
338.ssh/
339*.pub
340"#;
341
342        let gitignore_path = self.project_path.join(".gitignore");
343        fs::write(&gitignore_path, gitignore)?;
344        println!("  {} .gitignore", "Created:".green());
345
346        Ok(())
347    }
348}