use clap::{Args, Parser, Subcommand};
use regex::Regex;
use serde::Deserialize;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::{Duration, UNIX_EPOCH};
type AnyError = Box<dyn Error>;
type Result<T> = std::result::Result<T, AnyError>;
fn main() {
if let Err(err) = run_cli(Cli::parse()) {
eprintln!("{err}");
std::process::exit(1);
}
}
#[derive(Parser)]
#[command(name = "exe", about = "ExecuteSoft repository tooling")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Service {
#[command(subcommand)]
command: ServiceCommand,
},
Gateway {
#[command(subcommand)]
command: GatewayCommand,
},
Db {
#[command(subcommand)]
command: DbCommand,
},
Dev {
#[command(subcommand)]
command: DevCommand,
},
Sync,
Release(KeyValueArgs),
Deploy {
#[command(subcommand)]
command: DeployCommand,
},
}
#[derive(Subcommand)]
enum ServiceCommand {
Check { path: String },
CheckAll,
Create(CreateServiceArgs),
Test(ServiceTargetArgs),
Build(ServiceTargetArgs),
Generate(ServiceTargetArgs),
Run(ServiceTargetArgs),
Clippy(ServiceTargetArgs),
Compliance(ServiceTargetArgs),
GatewayRoutesCheck(ServiceTargetArgs),
}
#[derive(Args)]
struct CreateServiceArgs {
#[arg(long, alias = "service")]
name: String,
#[arg(long, default_value = "core")]
domain: String,
#[arg(long = "template", alias = "tpl", default_value = "go")]
template_lang: String,
#[arg(long = "type", default_value = "grpc")]
service_type: String,
#[arg(long, default_value = "native")]
framework: String,
#[arg(long)]
owner_team: Option<String>,
#[arg(long)]
output: Option<String>,
#[arg(long)]
template_name: Option<String>,
#[arg(long)]
proto_package: Option<String>,
#[arg(long)]
service_class: Option<String>,
}
#[derive(Args)]
struct ServiceTargetArgs {
target: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
vars: Vec<String>,
}
#[derive(Subcommand)]
enum GatewayCommand {
Generate,
Route {
#[command(subcommand)]
command: GatewayRouteCommand,
},
ValidateRoutes {
files: Vec<String>,
},
}
#[derive(Subcommand)]
enum GatewayRouteCommand {
Create(GatewayRouteArgs),
FromProto(GatewayRouteArgs),
Entry(GatewayRouteEntryArgs),
}
#[derive(Args)]
struct GatewayRouteArgs {
#[arg(long, alias = "name")]
service: String,
#[arg(long)]
force: bool,
#[arg(long)]
append: bool,
}
#[derive(Args)]
struct GatewayRouteEntryArgs {
#[arg(long, alias = "name")]
service: String,
#[arg(long)]
rpc: String,
#[arg(long)]
force: bool,
#[arg(long)]
append: bool,
}
#[derive(Subcommand)]
enum DbCommand {
Migrate(KeyValueArgs),
Up(KeyValueArgs),
Status(KeyValueArgs),
Revert(KeyValueArgs),
Down(KeyValueArgs),
Reset(KeyValueArgs),
New(KeyValueArgs),
}
#[derive(Subcommand)]
enum DevCommand {
Up(KeyValueArgs),
All(KeyValueArgs),
Build(KeyValueArgs),
Down(KeyValueArgs),
Logs(KeyValueArgs),
Reset(KeyValueArgs),
Watch(WatchArgs),
}
#[derive(Args)]
struct WatchArgs {
#[arg(long, default_value = "configs/app.env.example")]
env_file: String,
#[arg(long = "watch", default_value = ".")]
watch_root: String,
#[arg(long)]
ignore: Vec<String>,
#[arg(long, default_value_t = 1.0)]
poll: f64,
#[arg(last = true, required = true)]
command: Vec<String>,
}
#[derive(Subcommand)]
enum DeployCommand {
Kubernetes(KeyValueArgs),
Docker(KeyValueArgs),
}
#[derive(Args, Clone)]
struct KeyValueArgs {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
vars: Vec<String>,
}
fn run_cli(cli: Cli) -> Result<()> {
match cli.command {
Commands::Service { command } => run_service_command(command),
Commands::Gateway { command } => run_gateway_command(command),
Commands::Db { command } => run_db_command(command),
Commands::Dev { command } => run_dev_command(command),
Commands::Sync => run_sync(),
Commands::Release(args) => run_release(&args.vars),
Commands::Deploy { command } => run_deploy_command(command),
}
}
fn usage_error<T>(message: String) -> Result<T> {
Err(message.into())
}
fn repo_root() -> PathBuf {
if let Ok(root) = env::var("EXECUTESOFT_ROOT") {
return PathBuf::from(root);
}
let mut dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
loop {
if dir.join("contracts/protobuf").exists() && dir.join("services").exists() {
return dir;
}
if !dir.pop() {
return PathBuf::from(".");
}
}
}
fn repo_path(path: &str) -> PathBuf {
let raw = PathBuf::from(path);
if raw.is_absolute() || raw.exists() {
return raw;
}
let from_root = repo_root().join(path);
if from_root.exists() {
return from_root;
}
raw
}
fn run_cmd(dir: &Path, program: &str, args: &[String]) -> Result<()> {
let status = Command::new(program)
.args(args)
.current_dir(dir)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if status.success() {
Ok(())
} else {
Err(format!("{program} exited with {status}").into())
}
}
fn run_make(dir: &Path, target: &str, vars: &[String]) -> Result<()> {
let mut args = vec![target.to_string()];
args.extend(vars.iter().cloned());
run_cmd(dir, "make", &args)
}
fn run_service_command(command: ServiceCommand) -> Result<()> {
match command {
ServiceCommand::Check { path } => check_service(&repo_path(&path)),
ServiceCommand::CheckAll => check_all_services(),
ServiceCommand::Create(args) => create_service(args),
ServiceCommand::Test(args) => run_service_make("test", args),
ServiceCommand::Build(args) => run_service_make("build", args),
ServiceCommand::Generate(args) => run_service_make("generate", args),
ServiceCommand::Run(args) => run_service_make("run", args),
ServiceCommand::Clippy(args) => run_service_make("clippy", args),
ServiceCommand::Compliance(args) => run_service_make("compliance", args),
ServiceCommand::GatewayRoutesCheck(args) => run_service_make("gateway-routes-check", args),
}
}
fn run_service_make(target: &str, args: ServiceTargetArgs) -> Result<()> {
let dir = resolve_service_dir(&args.target)?;
run_make(&dir, target, &make_vars(&args.vars))
}
fn create_service(args: CreateServiceArgs) -> Result<()> {
let name = args.name;
let domain = args.domain;
let template_lang = args.template_lang;
let service_type = args.service_type;
let framework = args.framework;
let owner_team = args.owner_team.unwrap_or_else(|| format!("team-{name}"));
let output = args
.output
.unwrap_or_else(|| format!("services/{domain}/{name}"));
let template = format!("tools/templates/template-{template_lang}-{service_type}");
let root = repo_root();
let template_dir = root.join(&template);
if !template_dir.is_dir() {
return usage_error(format!("Template not found: {template}"));
}
let output_dir = repo_path_for_create(&root, &output);
let template_name = args.template_name.unwrap_or_else(|| {
template_dir
.file_name()
.and_then(OsStr::to_str)
.unwrap_or("template")
.to_string()
});
let proto_package = args
.proto_package
.unwrap_or_else(|| package_from_name(&domain, &name));
let service_class = args.service_class.unwrap_or_else(|| class_from_name(&name));
create_service_from_template(CreateServiceSpec {
root: &root,
template_dir: &template_dir,
output_dir: &output_dir,
service_name: &name,
domain: &domain,
owner_team: &owner_team,
framework: &framework,
template_name: &template_name,
proto_package: &proto_package,
service_class: &service_class,
})?;
check_service(&output_dir)
}
fn repo_path_for_create(root: &Path, value: &str) -> PathBuf {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
root.join(path)
}
}
struct CreateServiceSpec<'a> {
root: &'a Path,
template_dir: &'a Path,
output_dir: &'a Path,
service_name: &'a str,
domain: &'a str,
owner_team: &'a str,
framework: &'a str,
template_name: &'a str,
proto_package: &'a str,
service_class: &'a str,
}
fn create_service_from_template(spec: CreateServiceSpec<'_>) -> Result<()> {
let framework_skeleton = spec
.template_dir
.join("skeleton-frameworks")
.join(spec.framework);
let skeleton = if spec.framework != "native" && framework_skeleton.is_dir() {
framework_skeleton
} else {
spec.template_dir.join("skeleton")
};
if !skeleton.is_dir() {
return usage_error(format!(
"template skeleton does not exist: {}",
skeleton.display()
));
}
if spec.output_dir.exists() {
return usage_error(format!(
"output already exists: {}",
spec.output_dir.display()
));
}
copy_template_dir(&skeleton, spec.output_dir)?;
replace_template_tokens(spec.output_dir, &spec)?;
let local_contract = spec.output_dir.join("api/service.proto");
let shared_contract = spec.root.join(format!(
"contracts/protobuf/{}-{}.proto",
spec.domain, spec.service_name
));
if local_contract.is_file() {
if !shared_contract.exists() {
if let Some(parent) = shared_contract.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&local_contract, &shared_contract)?;
}
println!("created shared contract {}", shared_contract.display());
}
println!("created {}", spec.output_dir.display());
Ok(())
}
fn copy_template_dir(source: &Path, destination: &Path) -> Result<()> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if ["target", "__pycache__", ".pytest_cache"].contains(&name_str.as_ref()) {
continue;
}
let target = destination.join(name);
if path.is_dir() {
copy_template_dir(&path, &target)?;
} else if path.is_file() {
fs::copy(&path, &target)?;
}
}
Ok(())
}
fn replace_template_tokens(root: &Path, spec: &CreateServiceSpec<'_>) -> Result<()> {
let service_title = title_from_name(spec.service_name);
let service_title_lc = service_title.to_lowercase();
let crate_name = crate_from_name(spec.service_name);
let replacements = [
("__SERVICE_NAME__", spec.service_name.to_string()),
("__SERVICE_TITLE__", service_title),
("__SERVICE_TITLE_LC__", service_title_lc),
("__DOMAIN__", spec.domain.to_string()),
("__OWNER_TEAM__", spec.owner_team.to_string()),
("__FRAMEWORK__", spec.framework.to_string()),
("__TEMPLATE_NAME__", spec.template_name.to_string()),
("__PROTO_PACKAGE__", spec.proto_package.to_string()),
("__SERVICE_CLASS__", spec.service_class.to_string()),
("template_service", crate_name),
("template-service", spec.service_name.to_string()),
];
replace_template_tokens_walk(root, &replacements)
}
fn replace_template_tokens_walk(root: &Path, replacements: &[(&str, String)]) -> Result<()> {
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
replace_template_tokens_walk(&path, replacements)?;
continue;
}
if !path.is_file() || !is_text_file(&path)? {
continue;
}
let mut text = fs::read_to_string(&path)?;
for (needle, replacement) in replacements {
text = text.replace(needle, replacement);
}
fs::write(&path, text)?;
}
Ok(())
}
fn is_text_file(path: &Path) -> Result<bool> {
let mut file = fs::File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
Ok(!buf.contains(&0) && std::str::from_utf8(&buf).is_ok())
}
fn title_from_name(value: &str) -> String {
value
.split(['-', '_'])
.filter(|part| !part.is_empty())
.map(capitalize)
.collect::<Vec<_>>()
.join(" ")
}
fn class_from_name(value: &str) -> String {
value
.split(['-', '_', '.'])
.filter(|part| !part.is_empty())
.map(capitalize)
.collect::<String>()
}
fn package_from_name(domain: &str, service_name: &str) -> String {
format!(
"executesoft.{}.{}.v1",
domain.replace(['-', '_'], "."),
service_name.replace(['-', '_'], ".")
)
}
fn crate_from_name(service_name: &str) -> String {
service_name.replace(['-', '.'], "_")
}
fn capitalize(value: &str) -> String {
let mut chars = value.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn check_all_services() -> Result<()> {
let root = repo_root();
let mut failed = false;
let mut service_yamls = Vec::new();
collect_template_service_yamls(&root.join("tools/templates"), &mut service_yamls)?;
collect_service_root_yamls(&root.join("services"), &mut service_yamls)?;
service_yamls.sort();
service_yamls.dedup();
for service_yaml in service_yamls {
if let Some(dir) = service_yaml.parent()
&& let Err(err) = check_service(dir)
{
failed = true;
eprintln!("{err}");
}
}
if failed {
Err("service compliance failed".into())
} else {
Ok(())
}
}
fn collect_template_service_yamls(templates_dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
if !templates_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(templates_dir)? {
let path = entry?.path();
if path.is_dir() {
let candidate = path.join("skeleton/service.yaml");
if candidate.exists() {
out.push(candidate);
}
}
}
Ok(())
}
fn collect_service_root_yamls(services_dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
if !services_dir.exists() {
return Ok(());
}
for domain in fs::read_dir(services_dir)? {
let domain = domain?.path();
if !domain.is_dir() {
continue;
}
let direct = domain.join("service.yaml");
if direct.exists() {
out.push(direct);
}
for service in fs::read_dir(&domain)? {
let service = service?.path();
if !service.is_dir() {
continue;
}
let candidate = service.join("service.yaml");
if candidate.exists() {
out.push(candidate);
}
}
}
Ok(())
}
fn check_service(service_dir: &Path) -> Result<()> {
let service_dir = service_dir
.canonicalize()
.unwrap_or_else(|_| service_dir.to_path_buf());
let failures = validate_service(&service_dir);
if failures.is_empty() {
println!("OK {}", service_dir.display());
Ok(())
} else {
for failure in &failures {
eprintln!("FAIL {}: {failure}", service_dir.display());
}
Err("service validation failed".into())
}
}
fn validate_service(service_dir: &Path) -> Vec<String> {
let mut failures = Vec::new();
if !service_dir.is_dir() {
return vec!["service directory does not exist".into()];
}
let metadata_path = service_dir.join("service.yaml");
if !metadata_path.exists() {
return vec!["service.yaml missing".into()];
}
let meta = parse_simple_yaml(&metadata_path);
for key in [
"name",
"domain",
"owner_team",
"runtime",
"framework",
"template",
"certified_template",
"api_type",
] {
if meta
.get(key)
.map(String::as_str)
.unwrap_or("")
.trim()
.is_empty()
{
failures.push(format!("service.yaml missing metadata key: {key}"));
}
}
if !failures.is_empty() {
return failures;
}
let name = meta.get("name").map(String::as_str).unwrap_or("");
let domain = meta.get("domain").map(String::as_str).unwrap_or("");
let runtime = meta.get("runtime").map(String::as_str).unwrap_or("");
let api_type = meta.get("api_type").map(String::as_str).unwrap_or("");
let is_gateway = domain == "core" && name == "gateway";
let is_cdn = domain == "core" && name == "cdn";
let allowed: HashSet<&str> = [
"api",
"cmd",
"configs",
"deploy",
"development",
"docs",
"gateway",
"migrations",
"observability",
"scripts",
"internal",
"seeds",
"src",
"tasks",
"tests",
"tools",
".agents",
".dockerignore",
"AGENTS.md",
"README.md",
"go.mod",
"go.sum",
"package.json",
"package-lock.json",
"tsconfig.json",
"pyproject.toml",
"Cargo.toml",
"Cargo.lock",
"build.rs",
"Dockerfile",
"Makefile",
"service.yaml",
"taskfile.yaml",
]
.into_iter()
.collect();
let ignored: HashSet<&str> = [
".git",
".pytest_cache",
"__pycache__",
"node_modules",
"target",
".DS_Store",
]
.into_iter()
.collect();
if let Ok(entries) = fs::read_dir(service_dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !ignored.contains(name.as_ref()) && !allowed.contains(name.as_ref()) {
failures.push(format!("top-level path is not allowed: {name}"));
}
}
}
if is_cdn {
require_files(
service_dir,
&mut failures,
&["Cargo.toml", "src/main.rs", "configs/cdn.toml"],
);
require_text(service_dir, &mut failures, "Dockerfile", &["EXPOSE 8090"]);
require_text(
service_dir,
&mut failures,
"README.md",
&["CDN Service", "DigitalOcean Spaces"],
);
return failures;
}
require_dirs(
service_dir,
&mut failures,
&[
"api",
"configs",
"deploy",
"development",
"docs",
"gateway",
"gateway/routes",
"migrations",
"observability",
"tasks",
"tests",
"docs/Overview",
"docs/Architecture",
"docs/Setup",
"docs/API",
"docs/Tasks",
"docs/Bugs",
"docs/Decisions",
"docs/Changelog",
"tests/contract",
],
);
require_files(
service_dir,
&mut failures,
&[
"AGENTS.md",
"Dockerfile",
"service.yaml",
".agents/skills/service-structure/SKILL.md",
"docs/Overview/README.md",
"docs/API/api.md",
"docs/API/api-development.md",
"docs/API/api-errors.md",
"docs/Setup/developer-onboarding.md",
"docs/Setup/runbook.md",
"tasks/README.md",
"configs/app.env.example",
"development/README.md",
"development/compose.dev.yml",
"gateway/README.md",
"deploy/deployment.yaml",
"deploy/service.yaml",
"deploy/hpa.yaml",
"deploy/pdb.yaml",
"deploy/network-policy.yaml",
"deploy/service-account.yaml",
"deploy/configmap.yaml",
"deploy/external-secret.yaml",
"observability/alerts.yaml",
"observability/dashboard.md",
],
);
if !has_matching_file(&service_dir.join("gateway/routes"), "-routes.json") {
failures.push("gateway route request missing: gateway/routes/*-routes.json".into());
}
if !service_dir.join("Makefile").exists() && !service_dir.join("taskfile.yaml").exists() {
failures
.push("one required file missing from alternatives: Makefile, taskfile.yaml".into());
}
if !service_dir.join("src").exists() && !service_dir.join("cmd").exists() {
failures.push("one application directory is required: src, cmd".into());
}
if is_gateway {
match runtime {
"go" => {
require_dirs(
service_dir,
&mut failures,
&[
"internal/bootstrap",
"internal/infrastructure/config",
"internal/infrastructure/logger",
"internal/interfaces/http",
],
);
require_files(
service_dir,
&mut failures,
&[
"internal/bootstrap/container.go",
"internal/bootstrap/infrastructure.go",
"internal/bootstrap/handlers.go",
"internal/bootstrap/server.go",
],
);
}
"rust" => {
require_dirs(
service_dir,
&mut failures,
&[
"src/bootstrap",
"src/infrastructure/config",
"src/infrastructure/logger",
"src/interfaces/http",
],
);
require_files(
service_dir,
&mut failures,
&[
"Cargo.toml",
"src/bootstrap/container.rs",
"src/bootstrap/infrastructure.rs",
"src/bootstrap/handlers.rs",
"src/bootstrap/server.rs",
"src/bootstrap/mod.rs",
"src/main.rs",
],
);
}
_ => {}
}
require_files(
service_dir,
&mut failures,
&[
"api/routes.yaml",
"api/public-routes.json",
"api/openapi.yaml",
"docs/Architecture/public-api.md",
],
);
require_text(
service_dir,
&mut failures,
"api/openapi.yaml",
&[
"openapi: 3.1.0",
"bearerAuth:",
"x-target-service:",
"x-target-rpc:",
],
);
} else {
match runtime {
"go" => {
require_dirs(
service_dir,
&mut failures,
&[
"internal/domain",
"internal/application",
"internal/infrastructure",
"internal/infrastructure/cache",
"internal/infrastructure/persistence",
"internal/infrastructure/persistence/database",
"internal/interfaces",
"internal/bootstrap",
],
);
require_files(
service_dir,
&mut failures,
&[
"go.mod",
"internal/bootstrap/container.go",
"internal/bootstrap/infrastructure.go",
"internal/bootstrap/repositories.go",
"internal/bootstrap/use_cases.go",
"internal/bootstrap/handlers.go",
"internal/bootstrap/server.go",
"cmd/server/main.go",
],
);
}
"typescript" => {
require_dirs(
service_dir,
&mut failures,
&[
"src/domain",
"src/application",
"src/infrastructure",
"src/infrastructure/cache",
"src/infrastructure/persistence",
"src/infrastructure/persistence/database",
"src/interfaces",
"src/bootstrap",
],
);
require_files(
service_dir,
&mut failures,
&[
"package.json",
"src/bootstrap/container.ts",
"src/bootstrap/infrastructure.ts",
"src/bootstrap/repositories.ts",
"src/bootstrap/use-cases.ts",
"src/bootstrap/handlers.ts",
"src/bootstrap/server.ts",
"src/server.ts",
],
);
}
"python" => {
require_dirs(
service_dir,
&mut failures,
&[
"src/domain",
"src/application",
"src/infrastructure",
"src/infrastructure/cache",
"src/infrastructure/persistence",
"src/infrastructure/persistence/database",
"src/interfaces",
"src/bootstrap",
],
);
require_files(
service_dir,
&mut failures,
&[
"pyproject.toml",
"src/bootstrap/container.py",
"src/bootstrap/infrastructure.py",
"src/bootstrap/repositories.py",
"src/bootstrap/use_cases.py",
"src/bootstrap/handlers.py",
"src/bootstrap/server.py",
"src/server.py",
],
);
}
"rust" => {
require_dirs(
service_dir,
&mut failures,
&[
"src/domain",
"src/application",
"src/infrastructure",
"src/infrastructure/cache",
"src/infrastructure/persistence",
"src/infrastructure/persistence/database",
"src/interfaces",
"src/bootstrap",
],
);
require_files(
service_dir,
&mut failures,
&[
"Cargo.toml",
"src/domain/model.rs",
"src/bootstrap/container.rs",
"src/bootstrap/infrastructure.rs",
"src/bootstrap/repositories.rs",
"src/bootstrap/use_cases.rs",
"src/bootstrap/handlers.rs",
"src/bootstrap/server.rs",
"src/bootstrap/mod.rs",
"src/main.rs",
],
);
}
_ => {}
}
}
for (key, value) in parse_exposes(&metadata_path) {
if value.is_empty() {
failures.push(format!("service.yaml exposes.{key} must be a path string"));
} else if !service_dir.join(&value).exists() {
failures.push(format!("service.yaml exposes path missing: {key}: {value}"));
}
}
match api_type {
"grpc" => {
if !has_matching_file(&service_dir.join("api"), ".proto") {
failures.push("required grpc contract glob missing: one of api/*.proto".into());
}
if !parse_exposes(&metadata_path).contains_key("protobuf") {
failures.push("service.yaml exposes must include key: protobuf".into());
}
let shared = repo_root().join(format!("contracts/protobuf/{domain}-{name}.proto"));
if !metadata_has_placeholder(&metadata_path) && !shared.exists() {
failures.push(format!(
"shared contract missing: contracts/protobuf/{domain}-{name}.proto"
));
}
require_text(service_dir, &mut failures, "Dockerfile", &["EXPOSE"]);
require_text(
service_dir,
&mut failures,
"deploy/deployment.yaml",
&["containerPort:", "name: grpc"],
);
require_text(
service_dir,
&mut failures,
"deploy/service.yaml",
&["port:", "name: grpc"],
);
require_text(
service_dir,
&mut failures,
"docs/API/api.md",
&[
"Local contract:",
"Shared contract:",
"gRPC services:",
"RPC methods:",
],
);
}
"rest" => {
if !has_matching_file(&service_dir.join("api"), ".yaml")
&& !has_matching_file(&service_dir.join("api"), ".yml")
{
failures.push(
"required rest contract glob missing: one of api/*.yaml, api/*.yml".into(),
);
}
if !parse_exposes(&metadata_path).contains_key("openapi") {
failures.push("service.yaml exposes must include key: openapi".into());
}
}
_ => {}
}
if metadata_has_placeholder(&metadata_path) {
require_text(
service_dir,
&mut failures,
"configs/app.env.example",
&[
"LOG_LEVEL=",
"LOG_FORMAT=json",
"SERVICE_DOMAIN=",
"SERVICE_ENVIRONMENT=",
"DB_DRIVER=",
"DATABASE_URL=",
"CACHE_DRIVER=",
"CACHE_URL=",
"CACHE_PREFIX=",
"OTEL_SERVICE_NAME=",
"OTEL_EXPORTER_OTLP_ENDPOINT=",
"OTEL_TRACES_EXPORTER=otlp",
"OTEL_METRICS_EXPORTER=otlp",
"OTEL_LOGS_EXPORTER=otlp",
],
);
}
require_text(
service_dir,
&mut failures,
"deploy/configmap.yaml",
&[
"DB_DRIVER:",
"CACHE_DRIVER:",
"CACHE_PREFIX:",
"SERVICE_DOMAIN:",
"SERVICE_ENVIRONMENT:",
"LOG_FORMAT:",
],
);
require_text(
service_dir,
&mut failures,
"deploy/deployment.yaml",
&[
"envFrom:",
"configMapRef:",
"secretRef:",
"-runtime-secrets",
],
);
require_text(
service_dir,
&mut failures,
"docs/API/api-errors.md",
&[
"contracts/protobuf/common/error.proto",
"ErrorResponse",
"INVALID_ARGUMENT",
"UNAUTHENTICATED",
"PERMISSION_DENIED",
"NOT_FOUND",
"INTERNAL",
],
);
require_text(
service_dir,
&mut failures,
"docs/API/api.md",
&[
"Owner team:",
"Protocol:",
"Development endpoint:",
"Auth",
"Error Model",
"Consumer Workflow",
"Do not use another team's source code",
],
);
require_text(
service_dir,
&mut failures,
"docs/API/api-development.md",
&[
"New API Development Process",
"gateway-routes.json",
"make generate",
"Do not manually edit",
"Golden Rules",
],
);
if !is_gateway && meta.get("certified_template").map(String::as_str) != Some("true") {
failures.push("service.yaml certified_template must be true".into());
}
failures
}
fn require_files(root: &Path, failures: &mut Vec<String>, files: &[&str]) {
for file in files {
if !root.join(file).is_file() {
failures.push(format!("required file missing: {file}"));
}
}
}
fn require_dirs(root: &Path, failures: &mut Vec<String>, dirs: &[&str]) {
for dir in dirs {
if !root.join(dir).is_dir() {
failures.push(format!("required directory missing: {dir}"));
}
}
}
fn require_text(root: &Path, failures: &mut Vec<String>, file: &str, snippets: &[&str]) {
let path = root.join(file);
let Ok(text) = fs::read_to_string(&path) else {
failures.push(format!("required text file missing: {file}"));
return;
};
for snippet in snippets {
if !text.contains(snippet) {
failures.push(format!("{file} missing required text: {snippet}"));
}
}
}
fn has_matching_file(dir: &Path, suffix: &str) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return false;
};
entries.flatten().any(|entry| {
entry.path().is_file() && entry.file_name().to_string_lossy().ends_with(suffix)
})
}
fn parse_simple_yaml(path: &Path) -> HashMap<String, String> {
let text = fs::read_to_string(path).unwrap_or_default();
let mut values = HashMap::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with('#')
|| line.starts_with(' ')
|| line.starts_with('\t')
{
continue;
}
if let Some((key, value)) = line.split_once(':') {
values.insert(
key.trim().to_string(),
value.trim().trim_matches(['"', '\'']).to_string(),
);
}
}
values
}
fn parse_exposes(path: &Path) -> HashMap<String, String> {
let text = fs::read_to_string(path).unwrap_or_default();
let mut out = HashMap::new();
let mut in_exposes = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "exposes:" {
in_exposes = true;
continue;
}
if in_exposes && !trimmed.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') {
break;
}
if in_exposes
&& trimmed.starts_with("- ")
&& let Some((key, value)) = trimmed.trim_start_matches("- ").split_once(':')
{
out.insert(
key.trim().to_string(),
value.trim().trim_matches(['"', '\'']).to_string(),
);
}
}
out
}
fn metadata_has_placeholder(path: &Path) -> bool {
parse_simple_yaml(path)
.values()
.any(|value| value.starts_with("__"))
}
fn resolve_service_dir(value: &str) -> Result<PathBuf> {
if value.contains('/') || value.starts_with('.') {
let path = repo_path(value);
if !path.join("Makefile").exists() {
return usage_error(format!("service Makefile not found: {value}"));
}
return Ok(path);
}
let mut matches = BTreeSet::new();
let mut service_yamls = Vec::new();
collect_service_root_yamls(&repo_root().join("services"), &mut service_yamls)?;
for service_yaml in service_yamls {
if let Some(dir) = service_yaml.parent()
&& dir.file_name().and_then(OsStr::to_str) == Some(value)
{
matches.insert(dir.to_path_buf());
}
}
match matches.len() {
0 => usage_error(format!("service not found: {value}")),
1 => Ok(matches.into_iter().next().unwrap()),
_ => usage_error(format!("service name is ambiguous: {value}")),
}
}
fn run_gateway_command(command: GatewayCommand) -> Result<()> {
match command {
GatewayCommand::Generate => {
run_make(&repo_root().join("services/core/gateway"), "generate", &[])
}
GatewayCommand::Route { command } => run_gateway_route_command(command),
GatewayCommand::ValidateRoutes { files } => validate_gateway_routes(&files),
}
}
fn run_gateway_route_command(command: GatewayRouteCommand) -> Result<()> {
match command {
GatewayRouteCommand::Create(args) => {
run_gateway_route_make("route-file", args.service, None, args.force, args.append)
}
GatewayRouteCommand::FromProto(args) => run_gateway_route_make(
"route-file-from-proto",
args.service,
None,
args.force,
args.append,
),
GatewayRouteCommand::Entry(args) => run_gateway_route_make(
"route-entry",
args.service,
Some(args.rpc),
args.force,
args.append,
),
}
}
fn run_gateway_route_make(
target: &str,
service: String,
rpc: Option<String>,
force: bool,
append: bool,
) -> Result<()> {
let mut vars = vec![format!("SERVICE={service}")];
if let Some(rpc) = rpc {
vars.push(format!("RPC={rpc}"));
}
if force {
vars.push("FORCE=true".into());
}
if append {
vars.push("APPEND=true".into());
}
run_make(&repo_root().join("services/core/gateway"), target, &vars)
}
#[derive(Deserialize)]
struct RouteModule {
version: i64,
routes: Vec<RouteEntry>,
}
#[derive(Deserialize)]
struct RouteEntry {
id: String,
public: PublicRoute,
target: TargetRoute,
}
#[derive(Deserialize)]
struct PublicRoute {
method: String,
path: String,
auth: String,
#[serde(default)]
permission: String,
}
#[derive(Deserialize)]
struct TargetRoute {
protocol: String,
proto: String,
package: String,
service: String,
rpc: String,
endpoint_env: String,
}
fn validate_gateway_routes(args: &[String]) -> Result<()> {
let files: Vec<PathBuf> = if args.is_empty() {
let routes_dir = env::current_dir()?.join("gateway/routes");
fs::read_dir(routes_dir)?
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| name.ends_with("-routes.json"))
})
.collect()
} else {
args.iter().map(|arg| repo_path(arg)).collect()
};
if files.is_empty() {
println!("No gateway route request files found.");
return Ok(());
}
for file in &files {
validate_gateway_route_file(file)?;
}
println!("OK gateway route request files: {}", files.len());
Ok(())
}
fn validate_gateway_route_file(path: &Path) -> Result<()> {
let text = fs::read_to_string(path)?;
let module: RouteModule = serde_json::from_str(&text)?;
if module.version != 1 {
return usage_error(format!("{}: version must be 1", path.display()));
}
let mut seen = HashSet::new();
for route in module.routes {
if route.id.is_empty() {
return usage_error(format!("{}: route id is required", path.display()));
}
if !seen.insert(route.id.clone()) {
return usage_error(format!(
"{}: duplicate route id: {}",
path.display(),
route.id
));
}
if !["GET", "POST", "PUT", "PATCH", "DELETE"].contains(&route.public.method.as_str()) {
return usage_error(format!(
"{}: {}.public.method is invalid",
path.display(),
route.id
));
}
if !route.public.path.starts_with('/') {
return usage_error(format!(
"{}: {}.public.path must start with /",
path.display(),
route.id
));
}
if route.public.auth != "public" && route.public.auth != "permission" {
return usage_error(format!(
"{}: {}.public.auth must be public or permission",
path.display(),
route.id
));
}
if route.public.auth == "permission" && route.public.permission.is_empty() {
return usage_error(format!(
"{}: {}.public.permission is required for permission auth",
path.display(),
route.id
));
}
if route.target.protocol != "grpc" {
return usage_error(format!(
"{}: {}.target.protocol must be grpc",
path.display(),
route.id
));
}
for (field, value) in [
("proto", route.target.proto),
("package", route.target.package),
("service", route.target.service),
("rpc", route.target.rpc),
("endpoint_env", route.target.endpoint_env),
] {
if value.is_empty() {
return usage_error(format!(
"{}: {}.target.{field} is required",
path.display(),
route.id
));
}
}
}
Ok(())
}
fn run_db_command(command: DbCommand) -> Result<()> {
let (action, vars) = match command {
DbCommand::Migrate(args) => ("migrate", args.vars),
DbCommand::Up(args) => ("migrate", args.vars),
DbCommand::Status(args) => ("status", args.vars),
DbCommand::Revert(args) => ("revert", args.vars),
DbCommand::Down(args) => ("revert", args.vars),
DbCommand::Reset(args) => ("reset", args.vars),
DbCommand::New(args) => ("new", args.vars),
};
for arg in vars {
if let Some((key, value)) = arg.split_once('=') {
unsafe { env::set_var(key, value) };
}
}
let cfg = MigrationConfig::from_env();
match action {
"migrate" | "up" => apply_migrations(&cfg),
"status" => migration_status(&cfg),
"revert" | "down" => revert_migration(&cfg),
"reset" => reset_database(&cfg),
"new" => create_migration(&cfg.dir),
other => usage_error(format!("unknown db command: {other}")),
}
}
struct MigrationConfig {
dir: PathBuf,
container: String,
user: String,
database: String,
schema: String,
table: String,
}
impl MigrationConfig {
fn from_env() -> Self {
Self {
dir: PathBuf::from(env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "migrations".into())),
container: env::var("MIGRATION_DB_CONTAINER")
.unwrap_or_else(|_| "executesoft-dev-postgres-1".into()),
user: env::var("MIGRATION_DB_USER").unwrap_or_else(|_| "executesoft".into()),
database: env::var("MIGRATION_DB_NAME").unwrap_or_default(),
schema: env::var("MIGRATION_SCHEMA").unwrap_or_else(|_| "public".into()),
table: env::var("MIGRATION_TABLE").unwrap_or_else(|_| "schema_migrations".into()),
}
}
}
fn require_database(cfg: &MigrationConfig) -> Result<()> {
if cfg.database.is_empty() {
usage_error("MIGRATION_DB_NAME is required".into())
} else {
Ok(())
}
}
fn docker_psql(
cfg: &MigrationConfig,
database: &str,
extra: &[&str],
stdin: Option<&str>,
) -> Result<String> {
let mut cmd = Command::new("docker");
cmd.args([
"exec",
"-i",
&cfg.container,
"psql",
"-v",
"ON_ERROR_STOP=1",
"-U",
&cfg.user,
"-d",
database,
]);
cmd.args(extra);
cmd.stderr(Stdio::inherit());
if stdin.is_some() {
cmd.stdin(Stdio::piped());
}
cmd.stdout(Stdio::piped());
let mut child = cmd.spawn()?;
if let Some(input) = stdin {
child.stdin.as_mut().unwrap().write_all(input.as_bytes())?;
}
let output = child.wait_with_output()?;
if !output.status.success() {
return Err(format!("docker psql exited with {}", output.status).into());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn ensure_database(cfg: &MigrationConfig) -> Result<()> {
require_database(cfg)?;
let sql = "SELECT format('CREATE DATABASE %I', :'db_name') WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db_name')\\gexec\n".to_string();
docker_psql(
cfg,
"postgres",
&["-v", &format!("db_name={}", cfg.database)],
Some(&sql),
)?;
Ok(())
}
fn psql(cfg: &MigrationConfig, extra: &[&str], stdin: Option<&str>) -> Result<String> {
ensure_database(cfg)?;
docker_psql(cfg, &cfg.database, extra, stdin)
}
fn ensure_tracking_table(cfg: &MigrationConfig) -> Result<()> {
let sql = format!(
"CREATE TABLE IF NOT EXISTS {}.{} (version TEXT PRIMARY KEY, filename TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT now());",
cfg.schema, cfg.table
);
psql(cfg, &[], Some(&sql)).map(|_| ())
}
fn migration_files(dir: &Path) -> Result<Vec<PathBuf>> {
if !dir.exists() {
println!("No migrations directory found: {}", dir.display());
return Ok(Vec::new());
}
let mut files = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if path.is_file()
&& name.ends_with(".sql")
&& !name.ends_with(".down.sql")
&& !name.ends_with("_down.sql")
{
files.push(path);
}
}
files.sort();
Ok(files)
}
fn migration_version(path: &Path) -> String {
path.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string()
}
fn apply_migrations(cfg: &MigrationConfig) -> Result<()> {
let files = migration_files(&cfg.dir)?;
if files.is_empty() {
println!("No migration files found in {}.", cfg.dir.display());
return Ok(());
}
ensure_tracking_table(cfg)?;
let mut applied = 0;
for file in files {
let version = migration_version(&file);
let exists = psql(
cfg,
&[
"-At",
"-c",
&format!(
"SELECT EXISTS (SELECT 1 FROM {}.{} WHERE version = '{}');",
cfg.schema,
cfg.table,
sql_literal(&version)
),
],
None,
)?;
if exists == "t" {
println!("Skipping already applied migration: {version}");
continue;
}
println!("Applying migration: {version}");
let sql = fs::read_to_string(&file)?;
psql(cfg, &[], Some(&sql))?;
psql(
cfg,
&[
"-c",
&format!(
"INSERT INTO {}.{} (version, filename) VALUES ('{}', '{}');",
cfg.schema,
cfg.table,
sql_literal(&version),
sql_literal(file.file_name().unwrap().to_string_lossy().as_ref())
),
],
None,
)?;
applied += 1;
}
if applied == 0 {
println!("No pending migrations.");
}
Ok(())
}
fn migration_status(cfg: &MigrationConfig) -> Result<()> {
let files = migration_files(&cfg.dir)?;
if files.is_empty() {
println!("No migration files found in {}.", cfg.dir.display());
return Ok(());
}
ensure_tracking_table(cfg)?;
for file in files {
let version = migration_version(&file);
let exists = psql(
cfg,
&[
"-At",
"-c",
&format!(
"SELECT EXISTS (SELECT 1 FROM {}.{} WHERE version = '{}');",
cfg.schema,
cfg.table,
sql_literal(&version)
),
],
None,
)?;
println!(
"{} {version}",
if exists == "t" { "applied" } else { "pending" }
);
}
Ok(())
}
fn down_file_for(dir: &Path, version: &str) -> Option<PathBuf> {
[
dir.join("down").join(format!("{version}.sql")),
dir.join(format!("{version}.down.sql")),
dir.join(format!("{version}_down.sql")),
]
.into_iter()
.find(|path| path.exists())
}
fn revert_migration(cfg: &MigrationConfig) -> Result<()> {
ensure_tracking_table(cfg)?;
let latest = psql(
cfg,
&[
"-At",
"-c",
&format!(
"SELECT version FROM {}.{} ORDER BY applied_at DESC, version DESC LIMIT 1;",
cfg.schema, cfg.table
),
],
None,
)?;
if latest.is_empty() {
println!("No applied migrations to revert.");
return Ok(());
}
let down = down_file_for(&cfg.dir, &latest)
.ok_or_else(|| format!("No down migration found for {latest}"))?;
println!("Reverting migration: {latest}");
psql(cfg, &[], Some(&fs::read_to_string(down)?))?;
psql(
cfg,
&[
"-c",
&format!(
"DELETE FROM {}.{} WHERE version = '{}';",
cfg.schema,
cfg.table,
sql_literal(&latest)
),
],
None,
)?;
Ok(())
}
fn reset_database(cfg: &MigrationConfig) -> Result<()> {
if env::var("CONFIRM").ok().as_deref() != Some("yes") {
return usage_error("Refusing to reset database without CONFIRM=yes".into());
}
println!(
"Resetting schema {} in database {}.",
cfg.schema, cfg.database
);
psql(
cfg,
&[],
Some(&format!(
"DROP SCHEMA IF EXISTS {} CASCADE; CREATE SCHEMA {};",
cfg.schema, cfg.schema
)),
)?;
apply_migrations(cfg)
}
fn create_migration(dir: &Path) -> Result<()> {
let name =
env::var("NAME").map_err(|_| "NAME is required. Example: exe db new NAME=create_orders")?;
fs::create_dir_all(dir.join("down"))?;
let ts = Command::new("date").arg("+%Y%m%d%H%M%S").output()?;
let timestamp = String::from_utf8_lossy(&ts.stdout).trim().to_string();
let safe = Regex::new("[^a-z0-9_]+")?
.replace_all(&name.to_lowercase(), "_")
.trim_matches('_')
.to_string();
let version = format!("{timestamp}_{safe}");
fs::write(
dir.join(format!("{version}.sql")),
"-- Add migration SQL here.\n",
)?;
fs::write(
dir.join("down").join(format!("{version}.sql")),
"-- Add rollback SQL here.\n",
)?;
println!("Created {}/{}.sql", dir.display(), version);
println!("Created {}/down/{}.sql", dir.display(), version);
Ok(())
}
fn sql_literal(value: &str) -> String {
value.replace('\'', "''")
}
fn run_dev_command(command: DevCommand) -> Result<()> {
match command {
DevCommand::Up(_) => docker_compose(&["up", "-d", "postgres", "redis", "nats"]),
DevCommand::All(_) => docker_compose(&["up", "--build"]),
DevCommand::Build(_) => docker_compose(&["build"]),
DevCommand::Down(_) => docker_compose(&["down"]),
DevCommand::Logs(_) => docker_compose(&["logs", "-f", "postgres", "redis", "nats"]),
DevCommand::Reset(_) => docker_compose(&["down", "-v"]),
DevCommand::Watch(args) => run_dev_watch(args),
}
}
fn docker_compose(args: &[&str]) -> Result<()> {
let file = env::var("DEV_COMPOSE_FILE")
.unwrap_or_else(|_| "development/docker/compose.dev.yml".into());
let mut cmd_args = vec!["compose".to_string(), "-f".to_string(), file];
cmd_args.extend(args.iter().map(|arg| arg.to_string()));
run_cmd(&repo_root(), "docker", &cmd_args)
}
fn run_dev_watch(args: WatchArgs) -> Result<()> {
let mut ignored: HashSet<String> = [
"response-mappers.json",
"routes.yaml",
"public-routes.json",
"route-permissions.json",
"openapi.yaml",
]
.into_iter()
.map(String::from)
.collect();
for item in args.ignore {
ignored.insert(item);
}
if args.command.is_empty() {
return usage_error("dev command is required".into());
}
load_env_file(Path::new(&args.env_file));
let mut previous = snapshot(Path::new(&args.watch_root), &ignored)?;
let mut child = start_child(&args.command)?;
let poll = Duration::from_millis((args.poll * 1000.0) as u64);
loop {
thread::sleep(poll);
let current = snapshot(Path::new(&args.watch_root), &ignored)?;
if current != previous {
println!("[dev] change detected, restarting");
stop_child(&mut child);
child = start_child(&args.command)?;
previous = current;
}
}
}
fn load_env_file(path: &Path) {
let Ok(text) = fs::read_to_string(path) else {
return;
};
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
unsafe { env::set_var(key.trim(), value.trim().trim_matches(['"', '\''])) };
}
}
}
fn start_child(command: &[String]) -> Result<Child> {
println!("[dev] starting: {}", command.join(" "));
Ok(Command::new(&command[0])
.args(&command[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?)
}
fn stop_child(child: &mut Child) {
let _ = child.kill();
let _ = child.wait();
}
fn snapshot(root: &Path, ignored: &HashSet<String>) -> Result<String> {
let mut rows = Vec::new();
snapshot_walk(root, ignored, &mut rows)?;
rows.sort();
Ok(rows.join("\n"))
}
fn snapshot_walk(root: &Path, ignored: &HashSet<String>, rows: &mut Vec<String>) -> Result<()> {
if !root.exists() {
return Ok(());
}
let pruned: HashSet<&str> = [
".git",
".pytest_cache",
"__pycache__",
"build",
"coverage",
"dist",
"node_modules",
"target",
]
.into_iter()
.collect();
let allowed: HashSet<&str> = [
".go", ".mod", ".sum", ".ts", ".js", ".json", ".py", ".toml", ".rs", ".yaml", ".yml",
".proto",
]
.into_iter()
.collect();
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if path.is_dir() {
if !pruned.contains(name.as_str()) {
snapshot_walk(&path, ignored, rows)?;
}
} else if !ignored.contains(&name)
&& path
.extension()
.and_then(OsStr::to_str)
.is_some_and(|ext| allowed.contains(format!(".{ext}").as_str()))
{
let modified = entry
.metadata()?
.modified()?
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
rows.push(format!("{modified} {}", path.display()));
}
}
Ok(())
}
fn run_deploy_command(command: DeployCommand) -> Result<()> {
match command {
DeployCommand::Kubernetes(args) => {
run_sync()?;
run_make(
&repo_root().join("devops"),
"deploy-kubernetes",
&make_vars(&args.vars),
)
}
DeployCommand::Docker(args) => run_make(
&repo_root().join("devops"),
"deploy-docker",
&make_vars(&args.vars),
),
}
}
fn run_sync() -> Result<()> {
run_cmd(
&repo_root(),
"devops/droplet/scripts/sync-service-assets.sh",
&[],
)
}
fn run_release(args: &[String]) -> Result<()> {
run_make(&repo_root().join("devops"), "release", &make_vars(args))
}
fn make_vars(args: &[String]) -> Vec<String> {
let mut vars = Vec::new();
for arg in args {
if let Some(raw) = arg.strip_prefix("--") {
if let Some((key, value)) = raw.split_once('=') {
vars.push(format!(
"{}={}",
key.replace('-', "_").to_uppercase(),
value
));
}
} else if arg.contains('=') {
vars.push(arg.clone());
}
}
vars
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clap_parses_service_create() {
let cli = Cli::try_parse_from([
"exe",
"service",
"create",
"--name",
"orders",
"--domain",
"commerce",
"--template",
"rust",
])
.unwrap();
match cli.command {
Commands::Service {
command: ServiceCommand::Create(args),
} => {
assert_eq!(args.name, "orders");
assert_eq!(args.domain, "commerce");
assert_eq!(args.template_lang, "rust");
}
_ => panic!("unexpected command"),
}
}
#[test]
fn maps_make_vars() {
let vars = make_vars(&[
"tag=latest".into(),
"--image-tag=v1".into(),
"--ignored".into(),
]);
assert_eq!(vars, vec!["tag=latest", "IMAGE_TAG=v1"]);
}
#[test]
fn parses_exposes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("service.yaml");
fs::write(&path, "name: auth\nexposes:\n - protobuf: api/auth.proto\n - openapi: api/openapi.yaml\nruntime: rust\n").unwrap();
let exposes = parse_exposes(&path);
assert_eq!(exposes.get("protobuf").unwrap(), "api/auth.proto");
assert_eq!(exposes.get("openapi").unwrap(), "api/openapi.yaml");
}
#[test]
fn creates_service_from_template() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let template = root.join("tools/templates/template-go-grpc");
let skeleton = template.join("skeleton");
fs::create_dir_all(skeleton.join("api")).unwrap();
fs::write(
skeleton.join("service.yaml"),
"name: __SERVICE_NAME__\ndomain: __DOMAIN__\nowner_team: __OWNER_TEAM__\n",
)
.unwrap();
fs::write(
skeleton.join("api/service.proto"),
"package __PROTO_PACKAGE__;\nservice __SERVICE_CLASS__Service {}\n",
)
.unwrap();
let output = root.join("services/commerce/order-service");
create_service_from_template(CreateServiceSpec {
root,
template_dir: &template,
output_dir: &output,
service_name: "order-service",
domain: "commerce",
owner_team: "team-orders",
framework: "native",
template_name: "template-go-grpc",
proto_package: "executesoft.commerce.order.service.v1",
service_class: "OrderService",
})
.unwrap();
let metadata = fs::read_to_string(output.join("service.yaml")).unwrap();
assert!(metadata.contains("name: order-service"));
assert!(metadata.contains("domain: commerce"));
assert!(metadata.contains("owner_team: team-orders"));
let shared =
fs::read_to_string(root.join("contracts/protobuf/commerce-order-service.proto"))
.unwrap();
assert!(shared.contains("package executesoft.commerce.order.service.v1;"));
assert!(shared.contains("service OrderServiceService {}"));
}
}