thinkinglanguage 0.3.7

CLI entry point for ThinkingLanguage (tl run, tl shell)
// ThinkingLanguage — Deployment template generation
// Generates Dockerfile and Kubernetes manifests for .tl pipeline files.

use std::fs;
use std::path::Path;

/// Generate a Dockerfile for running a .tl pipeline.
pub fn generate_dockerfile(tl_file: &str) -> String {
    let filename = Path::new(tl_file)
        .file_name()
        .unwrap_or_default()
        .to_string_lossy();
    format!(
        r#"# Generated by ThinkingLanguage
FROM rust:1.83-slim AS builder

RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .
RUN cargo install --path crates/tl-cli

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/tl /usr/local/bin/tl
COPY {filename} /app/{filename}

WORKDIR /app
ENTRYPOINT ["tl", "run", "{filename}"]
"#
    )
}

/// Generate a Kubernetes Deployment + Service manifest.
pub fn generate_k8s_manifest(tl_file: &str) -> String {
    let filename = Path::new(tl_file)
        .file_name()
        .unwrap_or_default()
        .to_string_lossy();
    let app_name = Path::new(tl_file)
        .file_stem()
        .unwrap_or_default()
        .to_string_lossy()
        .replace('_', "-");
    format!(
        r#"# Generated by ThinkingLanguage
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {app_name}
  labels:
    app: {app_name}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {app_name}
  template:
    metadata:
      labels:
        app: {app_name}
    spec:
      containers:
      - name: {app_name}
        image: {app_name}:latest
        command: ["tl", "run", "{filename}"]
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: {app_name}
spec:
  selector:
    app: {app_name}
  ports:
  - port: 8080
    targetPort: 8080
"#
    )
}

/// Write deployment artifacts to an output directory.
pub fn write_deploy(tl_file: &str, target: &str, output_dir: &str) -> Result<(), String> {
    let out = Path::new(output_dir);
    fs::create_dir_all(out).map_err(|e| format!("Failed to create output dir: {e}"))?;

    match target {
        "docker" => {
            let content = generate_dockerfile(tl_file);
            let path = out.join("Dockerfile");
            fs::write(&path, &content).map_err(|e| format!("Failed to write Dockerfile: {e}"))?;
            println!("Generated {}", path.display());
        }
        "k8s" => {
            let content = generate_k8s_manifest(tl_file);
            let path = out.join("deployment.yaml");
            fs::write(&path, &content)
                .map_err(|e| format!("Failed to write deployment.yaml: {e}"))?;
            println!("Generated {}", path.display());
        }
        other => {
            return Err(format!(
                "Unknown deploy target: '{other}'. Use 'docker' or 'k8s'."
            ));
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_dockerfile() {
        let df = generate_dockerfile("pipeline.tl");
        assert!(df.contains("FROM rust:"));
        assert!(df.contains("pipeline.tl"));
        assert!(df.contains("ENTRYPOINT"));
        assert!(df.contains("tl"));
    }

    #[test]
    fn test_generate_k8s_manifest() {
        let manifest = generate_k8s_manifest("my_pipeline.tl");
        assert!(manifest.contains("apiVersion: apps/v1"));
        assert!(manifest.contains("kind: Deployment"));
        assert!(manifest.contains("my-pipeline"));
        assert!(manifest.contains("my_pipeline.tl"));
        assert!(manifest.contains("kind: Service"));
    }

    #[test]
    fn test_generate_dockerfile_nested_path() {
        let df = generate_dockerfile("examples/stream_01.tl");
        assert!(df.contains("stream_01.tl"));
    }
}