use crate::cli::{GatewayCommand, GatewayRouteCommand};
use crate::util::{Result, repo_path, repo_root, run_make, usage_error};
use serde::Deserialize;
use std::collections::HashSet;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) 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(())
}