use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use super::env::Env;
use super::eval::eval_apply;
use super::value::{LispError, Value};
use crate::compose::{
BindMount, ComposeFile, Dependency, HealthCheck, NetworkSpec, PortMapping, ServiceSpec,
VolumeMount,
};
pub type HookFn = Rc<dyn Fn() -> Result<(), LispError>>;
pub type HookMap = HashMap<String, Vec<HookFn>>;
#[derive(Default)]
pub struct PendingCompose {
pub spec: Option<ComposeFile>,
pub project: Option<String>,
pub foreground: bool,
}
pub fn register_pelagos_builtins(
env: &Env,
hooks: Rc<RefCell<HookMap>>,
pending: Rc<RefCell<PendingCompose>>,
) {
env.borrow_mut().define(
"service",
Value::Native(
"service".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
if args.is_empty() {
return Err(LispError::new("service: expected name"));
}
let name = str_or_sym("service", &args[0])?;
let mut spec = ServiceSpec {
name: name.clone(),
image: String::new(),
networks: Vec::new(),
volumes: Vec::new(),
bind_mounts: Vec::new(),
tmpfs_mounts: Vec::new(),
env: HashMap::new(),
ports: Vec::new(),
depends_on: Vec::new(),
memory: None,
cpus: None,
command: None,
workdir: None,
user: None,
cap_add: Vec::new(),
cap_drop: Vec::new(),
};
parse_service_opts(&mut spec, &args[1..])?;
if spec.image.is_empty() {
return Err(LispError::new(format!(
"service '{}': missing (image ...) option",
name
)));
}
Ok(Value::ServiceSpec(Box::new(spec)))
}),
),
);
env.borrow_mut().define(
"network",
Value::Native(
"network".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
if args.is_empty() {
return Err(LispError::new("network: expected name"));
}
let name = str_or_sym("network", &args[0])?;
let mut subnet = None;
for opt in &args[1..] {
if let Value::Pair(p) = opt {
if let Value::Symbol(key) = &p.0 {
if key == "subnet" {
if let Value::Pair(kv) = &p.1 {
subnet = Some(str_or_sym("network subnet", &kv.0)?);
}
}
}
}
}
Ok(Value::NetworkSpec(Box::new(NetworkSpec { name, subnet })))
}),
),
);
env.borrow_mut().define(
"volume",
Value::Native(
"volume".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
if args.is_empty() {
return Err(LispError::new("volume: expected name"));
}
let name = str_or_sym("volume", &args[0])?;
Ok(Value::VolumeSpec(name))
}),
),
);
env.borrow_mut().define(
"compose",
Value::Native(
"compose".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
let mut networks = Vec::new();
let mut volumes = Vec::new();
let mut services = Vec::new();
for arg in args {
collect_compose_items(arg, &mut networks, &mut volumes, &mut services)?;
}
Ok(Value::ComposeSpec(Box::new(ComposeFile {
networks,
volumes,
services,
})))
}),
),
);
{
let pending2 = Rc::clone(&pending);
env.borrow_mut().define(
"compose-up",
Value::Native(
"compose-up".into(),
Rc::new(move |args: &[Value]| -> Result<Value, LispError> {
if args.is_empty() {
return Err(LispError::new("compose-up: expected a compose spec"));
}
let spec = match &args[0] {
Value::ComposeSpec(c) => *c.clone(),
_ => {
return Err(LispError::new(format!(
"compose-up: expected compose-spec, got {}",
args[0].type_name()
)))
}
};
let project = args
.get(1)
.map(|v| match v {
Value::Str(s) | Value::Symbol(s) => Ok(s.clone()),
_ => Err(LispError::new("compose-up: project must be a string")),
})
.transpose()?;
let foreground = args.get(2).map(|v| v.is_truthy()).unwrap_or(false);
let mut p = pending2.borrow_mut();
p.spec = Some(spec);
p.project = project;
p.foreground = foreground;
Ok(Value::Nil)
}),
),
);
}
{
let hooks2 = Rc::clone(&hooks);
env.borrow_mut().define(
"on-ready",
Value::Native(
"on-ready".into(),
Rc::new(move |args: &[Value]| -> Result<Value, LispError> {
if args.len() != 2 {
return Err(LispError::new("on-ready: expected service name and lambda"));
}
let svc_name = str_or_sym("on-ready", &args[0])?;
let lambda = args[1].clone();
match &lambda {
Value::Lambda { .. } | Value::Native(_, _) => {}
_ => {
return Err(LispError::new(format!(
"on-ready: second argument must be a procedure, got {}",
lambda.type_name()
)))
}
}
let hook: HookFn = Rc::new(move || eval_apply(&lambda, &[]).map(|_| ()));
hooks2.borrow_mut().entry(svc_name).or_default().push(hook);
Ok(Value::Nil)
}),
),
);
}
env.borrow_mut().define(
"env",
Value::Native(
"env".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
if args.is_empty() {
return Err(LispError::new("env: expected variable name"));
}
let name = str_or_sym("env", &args[0])?;
match std::env::var(&name) {
Ok(v) => Ok(Value::Str(v)),
Err(_) => Ok(Value::Nil),
}
}),
),
);
env.borrow_mut().define(
"log",
Value::Native(
"log".into(),
Rc::new(|args: &[Value]| -> Result<Value, LispError> {
let parts: Vec<String> = args
.iter()
.map(|v| match v {
Value::Str(s) => s.clone(),
other => other.to_string(),
})
.collect();
log::info!("[lisp] {}", parts.join(" "));
Ok(Value::Nil)
}),
),
);
}
fn str_or_sym(ctx: &str, v: &Value) -> Result<String, LispError> {
match v {
Value::Str(s) | Value::Symbol(s) => Ok(s.clone()),
_ => Err(LispError::new(format!(
"{}: expected string or symbol, got {}",
ctx,
v.type_name()
))),
}
}
fn collect_compose_items(
val: &Value,
networks: &mut Vec<NetworkSpec>,
volumes: &mut Vec<String>,
services: &mut Vec<ServiceSpec>,
) -> Result<(), LispError> {
match val {
Value::ServiceSpec(s) => services.push(*s.clone()),
Value::NetworkSpec(n) => networks.push(*n.clone()),
Value::VolumeSpec(v) => volumes.push(v.clone()),
Value::Nil => {}
Value::Pair(_) => {
let items = val.to_vec()?;
for item in items {
collect_compose_items(&item, networks, volumes, services)?;
}
}
other => {
return Err(LispError::new(format!(
"compose: unexpected value {} ({})",
other,
other.type_name()
)))
}
}
Ok(())
}
fn parse_service_opts(spec: &mut ServiceSpec, opts: &[Value]) -> Result<(), LispError> {
for opt in opts {
match opt {
Value::Pair(_) => {
let items = opt.to_vec()?;
if items.is_empty() {
continue;
}
let key = str_or_sym("service option", &items[0])?;
let vals = &items[1..];
apply_service_opt(spec, &key, vals)?;
}
_ => {
return Err(LispError::new(format!(
"service option must be a list, got {}",
opt.type_name()
)))
}
}
}
Ok(())
}
fn apply_service_opt(spec: &mut ServiceSpec, key: &str, vals: &[Value]) -> Result<(), LispError> {
match key {
"image" => {
spec.image = str_or_sym_at("image", vals, 0)?;
}
"network" => {
for v in vals {
spec.networks.push(str_or_sym("network", v)?);
}
}
"env" => {
let k = str_or_sym_at("env key", vals, 0)?;
let v = str_or_sym_at("env value", vals, 1)?;
spec.env.insert(k, v);
}
"port" => {
let host = parse_port("port host", &vals[0])?;
let container = parse_port("port container", &vals[1])?;
spec.ports.push(PortMapping { host, container });
}
"depends-on" => {
let dep_name = str_or_sym_at("depends-on", vals, 0)?;
let health_check = match vals.get(1) {
Some(Value::Int(port)) => {
let p = u16::try_from(*port)
.map_err(|_| LispError::new("depends-on: port out of range"))?;
Some(HealthCheck::Port(p))
}
Some(_) => {
return Err(LispError::new(
"depends-on: optional second arg must be a port number",
))
}
None => None,
};
spec.depends_on.push(Dependency {
service: dep_name,
health_check,
});
}
"memory" => {
spec.memory = Some(str_or_sym_at("memory", vals, 0)?);
}
"cpus" => {
spec.cpus = Some(str_or_sym_at("cpus", vals, 0)?);
}
"command" => {
spec.command = Some(
vals.iter()
.map(|v| str_or_sym("command arg", v))
.collect::<Result<_, _>>()?,
);
}
"tmpfs" => {
let path = str_or_sym_at("tmpfs", vals, 0)?;
spec.tmpfs_mounts.push(path);
}
"volume" => {
let name = str_or_sym_at("volume name", vals, 0)?;
let mount_path = str_or_sym_at("volume mount_path", vals, 1)?;
spec.volumes.push(VolumeMount { name, mount_path });
}
"bind" => {
let host_path = str_or_sym_at("bind host_path", vals, 0)?;
let container_path = str_or_sym_at("bind container_path", vals, 1)?;
spec.bind_mounts.push(BindMount {
host_path,
container_path,
read_only: true,
});
}
"bind-rw" => {
let host_path = str_or_sym_at("bind-rw host_path", vals, 0)?;
let container_path = str_or_sym_at("bind-rw container_path", vals, 1)?;
spec.bind_mounts.push(BindMount {
host_path,
container_path,
read_only: false,
});
}
"workdir" => {
spec.workdir = Some(str_or_sym_at("workdir", vals, 0)?);
}
"user" => {
spec.user = Some(str_or_sym_at("user", vals, 0)?);
}
"cap-add" => {
for v in vals {
spec.cap_add.push(str_or_sym("cap-add", v)?);
}
}
"cap-drop" => {
for v in vals {
spec.cap_drop.push(str_or_sym("cap-drop", v)?);
}
}
other => {
return Err(LispError::new(format!(
"service: unknown option '{}'",
other
)))
}
}
Ok(())
}
fn str_or_sym_at(ctx: &str, vals: &[Value], idx: usize) -> Result<String, LispError> {
vals.get(idx)
.ok_or_else(|| LispError::new(format!("{}: missing value", ctx)))
.and_then(|v| str_or_sym(ctx, v))
}
fn parse_port(ctx: &str, v: &Value) -> Result<u16, LispError> {
match v {
Value::Int(n) => {
u16::try_from(*n).map_err(|_| LispError::new(format!("{}: port out of range", ctx)))
}
Value::Str(s) | Value::Symbol(s) => s
.parse::<u16>()
.map_err(|_| LispError::new(format!("{}: invalid port '{}'", ctx, s))),
_ => Err(LispError::new(format!("{}: expected integer port", ctx))),
}
}